Merge pull request #83 from guardicore/develop

Develop
This commit is contained in:
itaymmguardicore 2018-01-20 20:55:10 +02:00 committed by GitHub
commit 7f5734ac67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2267 additions and 174 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,8 +15,9 @@ from cc.resources.monkey_download import MonkeyDownload
from cc.resources.netmap import NetMap
from cc.resources.edge import Edge
from cc.resources.node import Node
from cc.resources.report import Report
from cc.resources.root import Root
from cc.resources.telemetry_feed import TelemetryFeed
from cc.services.config import ConfigService
__author__ = 'Barak'
@ -88,5 +89,7 @@ def init_app(mongo_url):
api.add_resource(NetMap, '/api/netmap', '/api/netmap/')
api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/')
api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/')
api.add_resource(Report, '/api/report', '/api/report/')
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
return app

View File

@ -2,3 +2,4 @@ __author__ = 'itay.mizeretz'
ISLAND_PORT = 5000
DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland"
DEBUG_SERVER = False

View File

@ -11,7 +11,7 @@ if BASE_PATH not in sys.path:
from cc.app import init_app
from cc.utils import local_ip_addresses
from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT
from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT, DEBUG_SERVER
from cc.database import is_db_server_up
if __name__ == '__main__':
@ -26,11 +26,13 @@ if __name__ == '__main__':
time.sleep(1)
app = init_app(mongo_url)
if DEBUG_SERVER:
app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key'))
else:
http_server = HTTPServer(WSGIContainer(app),
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
http_server.listen(ISLAND_PORT)
print('Monkey Island C&C Server is running on https://{}:{}'.format(local_ip_addresses()[0], ISLAND_PORT))
IOLoop.instance().start()
# app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key'))

View File

@ -36,7 +36,7 @@ def run_local_monkey():
# run the monkey
try:
args = ["%s m0nk3y -s %s:%s" % (target_path, local_ip_addresses()[0], ISLAND_PORT)]
args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], ISLAND_PORT)]
if sys.platform == "win32":
args = "".join(args)
pid = subprocess.Popen(args, shell=True).pid

View File

@ -53,6 +53,8 @@ class Monkey(flask_restful.Resource):
def post(self, **kw):
monkey_json = json.loads(request.data)
monkey_json['creds'] = []
monkey_json['dead'] = False
if 'keepalive' in monkey_json:
monkey_json['keepalive'] = dateutil.parser.parse(monkey_json['keepalive'])
else:
@ -60,6 +62,8 @@ class Monkey(flask_restful.Resource):
monkey_json['modifytime'] = datetime.now()
ConfigService.save_initial_config_if_needed()
# if new monkey telem, change config according to "new monkeys" config.
db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})
if not db_monkey:
@ -119,6 +123,8 @@ class Monkey(flask_restful.Resource):
node_id = existing_node["_id"]
for edge in mongo.db.edge.find({"to": node_id}):
mongo.db.edge.update({"_id": edge["_id"]}, {"$set": {"to": new_monkey_id}})
for creds in existing_node['creds']:
NodeService.add_credentials_to_monkey(new_monkey_id, creds)
mongo.db.node.remove({"_id": node_id})
return {"id": new_monkey_id}

View File

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

View File

@ -1,12 +1,12 @@
from datetime import datetime
from flask import request, make_response, jsonify
import flask_restful
from flask import request, make_response, jsonify
from cc.database import mongo
from cc.services.config import ConfigService
from cc.services.node import NodeService
from cc.services.report import ReportService
from cc.utils import local_ip_addresses
__author__ = 'Barak'
@ -18,24 +18,35 @@ class Root(flask_restful.Resource):
action = request.args.get('action')
if not action:
return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), completed_steps=self.get_completed_steps())
return Root.get_server_info()
elif action == "reset":
mongo.db.config.drop()
mongo.db.monkey.drop()
mongo.db.telemetry.drop()
mongo.db.node.drop()
mongo.db.edge.drop()
ConfigService.init_config()
return jsonify(status='OK')
return Root.reset_db()
elif action == "killall":
mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, upsert=False,
multi=True)
return jsonify(status='OK')
return Root.kill_all()
else:
return make_response(400, {'error': 'unknown action'})
def get_completed_steps(self):
@staticmethod
def get_server_info():
return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db),
completed_steps=Root.get_completed_steps())
@staticmethod
def reset_db():
[mongo.db[x].drop() for x in ['config', 'monkey', 'telemetry', 'node', 'edge', 'report']]
ConfigService.init_config()
return jsonify(status='OK')
@staticmethod
def kill_all():
mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}},
upsert=False,
multi=True)
return jsonify(status='OK')
@staticmethod
def get_completed_steps():
is_any_exists = NodeService.is_any_monkey_exists()
is_any_alive = NodeService.is_any_monkey_alive()
return dict(run_server=True, run_monkey=is_any_exists, infection_done=(is_any_exists and not is_any_alive))
infection_done = NodeService.is_monkey_finished_running()
report_done = ReportService.is_report_generated()
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)

View File

@ -1,5 +1,6 @@
import json
import traceback
import copy
from datetime import datetime
import dateutil
@ -39,7 +40,6 @@ class Telemetry(flask_restful.Resource):
telemetry_json = json.loads(request.data)
telemetry_json['timestamp'] = datetime.now()
telem_id = mongo.db.telemetry.insert(telemetry_json)
monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
try:
@ -53,6 +53,7 @@ class Telemetry(flask_restful.Resource):
print("Exception caught while processing telemetry: %s" % str(ex))
traceback.print_exc()
telem_id = mongo.db.telemetry.insert(telemetry_json)
return mongo.db.telemetry.find_one_or_404({"_id": telem_id})
@staticmethod
@ -70,6 +71,11 @@ class Telemetry(flask_restful.Resource):
monkey_label = telem_monkey_guid
x["monkey"] = monkey_label
objects.append(x)
if x['telem_type'] == 'system_info_collection' and 'credentials' in x['data']:
for user in x['data']['credentials']:
if -1 != user.find(','):
new_user = user.replace(',', '.')
x['data']['credentials'][new_user] = x['data']['credentials'].pop(user)
return objects
@ -103,7 +109,7 @@ class Telemetry(flask_restful.Resource):
@staticmethod
def process_exploit_telemetry(telemetry_json):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
new_exploit = telemetry_json['data']
new_exploit = copy.deepcopy(telemetry_json['data'])
new_exploit.pop('machine')
new_exploit['timestamp'] = telemetry_json['timestamp']
@ -115,10 +121,18 @@ class Telemetry(flask_restful.Resource):
if new_exploit['result']:
EdgeService.set_edge_exploited(edge)
for attempt in telemetry_json['data']['attempts']:
if attempt['result']:
found_creds = {'user': attempt['user']}
for field in ['password', 'lm_hash', 'ntlm_hash']:
if len(attempt[field]) != 0:
found_creds[field] = attempt[field]
NodeService.add_credentials_to_node(edge['to'], found_creds)
@staticmethod
def process_scan_telemetry(telemetry_json):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
data = telemetry_json['data']['machine']
data = copy.deepcopy(telemetry_json['data']['machine'])
ip_address = data.pop("ip_addr")
new_scan = \
{
@ -158,11 +172,17 @@ class Telemetry(flask_restful.Resource):
if 'ntlm_hash' in creds[user]:
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
for user in creds:
if -1 != user.find('.'):
new_user = user.replace('.', ',')
creds[new_user] = creds.pop(user)
@staticmethod
def process_trace_telemetry(telemetry_json):
# Nothing to do
return
TELEM_PROCESS_DICT = \
{
'tunnel': Telemetry.process_tunnel_telemetry,

View File

@ -0,0 +1,88 @@
from datetime import datetime
import dateutil
import flask_restful
from flask import request
import flask_pymongo
from cc.database import mongo
from cc.services.node import NodeService
__author__ = 'itay.mizeretz'
class TelemetryFeed(flask_restful.Resource):
def get(self, **kw):
timestamp = request.args.get('timestamp')
if "null" == timestamp or timestamp is None: # special case to avoid ugly JS code...
telemetries = mongo.db.telemetry.find({})
else:
telemetries = mongo.db.telemetry.find({'timestamp': {'$gt': dateutil.parser.parse(timestamp)}})\
telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)])
return \
{
'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries],
'timestamp': datetime.now().isoformat()
}
@staticmethod
def get_displayed_telemetry(telem):
return \
{
'id': telem['_id'],
'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'),
'hostname': NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'],
'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem)
}
@staticmethod
def get_tunnel_telem_brief(telem):
tunnel = telem['data']['proxy']
if tunnel is None:
return 'No tunnel is used.'
else:
tunnel_host_ip = tunnel.split(":")[-2].replace("//", "")
tunnel_host = NodeService.get_monkey_by_ip(tunnel_host_ip)['hostname']
return 'Tunnel set up to machine: %s.' % tunnel_host
@staticmethod
def get_state_telem_brief(telem):
if telem['data']['done']:
return 'Monkey died.'
else:
return 'Monkey started.'
@staticmethod
def get_exploit_telem_brief(telem):
target = telem['data']['machine']['ip_addr']
exploiter = telem['data']['exploiter']
result = telem['data']['result']
if result:
return 'Monkey successfully exploited %s using the %s exploiter.' % (target, exploiter)
else:
return 'Monkey failed exploiting %s using the %s exploiter.' % (target, exploiter)
@staticmethod
def get_scan_telem_brief(telem):
return 'Monkey discovered machine %s.' % telem['data']['machine']['ip_addr']
@staticmethod
def get_systeminfo_telem_brief(telem):
return 'Monkey collected system information.'
@staticmethod
def get_trace_telem_brief(telem):
return 'Monkey reached max depth.'
TELEM_PROCESS_DICT = \
{
'tunnel': TelemetryFeed.get_tunnel_telem_brief,
'state': TelemetryFeed.get_state_telem_brief,
'exploit': TelemetryFeed.get_exploit_telem_brief,
'scan': TelemetryFeed.get_scan_telem_brief,
'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief,
'trace': TelemetryFeed.get_trace_telem_brief
}

View File

@ -800,15 +800,23 @@ class ConfigService:
pass
@staticmethod
def get_config():
config = mongo.db.config.find_one({'name': 'newconfig'}) or {}
def get_config(is_initial_config=False):
config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {}
for field in ('name', '_id'):
config.pop(field, None)
return config
@staticmethod
def get_flat_config():
config_json = ConfigService.get_config()
def get_config_value(config_key_as_arr, is_initial_config=False):
config_key = reduce(lambda x, y: x+'.'+y, config_key_as_arr)
config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}, {config_key: 1})
for config_key_part in config_key_as_arr:
config = config[config_key_part]
return config
@staticmethod
def get_flat_config(is_initial_config=False):
config_json = ConfigService.get_config(is_initial_config)
flat_config_json = {}
for i in config_json:
for j in config_json[i]:
@ -880,6 +888,16 @@ class ConfigService:
config["cnc"]["servers"]["command_servers"] = ["%s:%d" % (ip, ISLAND_PORT) for ip in ips]
config["cnc"]["servers"]["current_server"] = "%s:%d" % (ips[0], ISLAND_PORT)
@staticmethod
def save_initial_config_if_needed():
if mongo.db.config.find_one({'name': 'initial'}) is not None:
return
initial_config = mongo.db.config.find_one({'name': 'newconfig'})
initial_config['name'] = 'initial'
initial_config.pop('_id')
mongo.db.config.insert(initial_config)
@staticmethod
def _extend_config_with_default(validator_class):
validate_properties = validator_class.VALIDATORS["properties"]

View File

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

View File

@ -12,11 +12,11 @@ class NodeService:
pass
@staticmethod
def get_displayed_node_by_id(node_id):
def get_displayed_node_by_id(node_id, for_report=False):
if ObjectId(node_id) == NodeService.get_monkey_island_pseudo_id():
return NodeService.get_monkey_island_node()
edges = EdgeService.get_displayed_edges_by_to(node_id)
edges = EdgeService.get_displayed_edges_by_to(node_id, for_report)
accessible_from_nodes = []
exploits = []
@ -29,14 +29,14 @@ class NodeService:
return new_node
# node is infected
new_node = NodeService.monkey_to_net_node(monkey)
new_node = NodeService.monkey_to_net_node(monkey, for_report)
for key in monkey:
if key not in ['_id', 'modifytime', 'parent', 'dead', 'description']:
new_node[key] = monkey[key]
else:
# node is uninfected
new_node = NodeService.node_to_net_node(node)
new_node = NodeService.node_to_net_node(node, for_report)
new_node["ip_addresses"] = node["ip_addresses"]
for edge in edges:
@ -89,6 +89,10 @@ class NodeService:
return True
@staticmethod
def get_monkey_label_by_id(monkey_id):
return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id))
@staticmethod
def get_monkey_label(monkey):
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
@ -115,22 +119,24 @@ class NodeService:
return "%s_%s" % (node_type, node_os)
@staticmethod
def monkey_to_net_node(monkey):
def monkey_to_net_node(monkey, for_report=False):
label = monkey['hostname'] if for_report else NodeService.get_monkey_label(monkey)
return \
{
"id": monkey["_id"],
"label": NodeService.get_monkey_label(monkey),
"label": label,
"group": NodeService.get_monkey_group(monkey),
"os": NodeService.get_monkey_os(monkey),
"dead": monkey["dead"],
}
@staticmethod
def node_to_net_node(node):
def node_to_net_node(node, for_report=False):
label = node['os']['version'] if for_report else NodeService.get_node_label(node)
return \
{
"id": node["_id"],
"label": NodeService.get_node_label(node),
"label": label,
"group": NodeService.get_node_group(node),
"os": NodeService.get_node_os(node)
}
@ -166,6 +172,7 @@ class NodeService:
{
"ip_addresses": [ip_address],
"exploited": False,
"creds": [],
"os":
{
"type": "unknown",
@ -273,3 +280,39 @@ class NodeService:
@staticmethod
def is_any_monkey_exists():
return mongo.db.monkey.find_one({}) is not None
@staticmethod
def is_monkey_finished_running():
return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive()
@staticmethod
def add_credentials_to_monkey(monkey_id, creds):
mongo.db.monkey.update(
{'_id': monkey_id},
{'$push': {'creds': creds}}
)
@staticmethod
def add_credentials_to_node(node_id, creds):
mongo.db.node.update(
{'_id': node_id},
{'$push': {'creds': creds}}
)
@staticmethod
def get_node_or_monkey_by_ip(ip_address):
node = NodeService.get_node_by_ip(ip_address)
if node is not None:
return node
return NodeService.get_monkey_by_ip(ip_address)
@staticmethod
def get_node_or_monkey_by_id(node_id):
node = NodeService.get_node_by_id(node_id)
if node is not None:
return node
return NodeService.get_monkey_by_id(node_id)
@staticmethod
def get_node_hostname(node):
return node['hostname'] if 'hostname' in node else node['os']['version']

View File

@ -0,0 +1,418 @@
import ipaddress
from enum import Enum
from cc.database import mongo
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.utils import local_ip_addresses, get_subnets
__author__ = "itay.mizeretz"
class ReportService:
def __init__(self):
pass
EXPLOIT_DISPLAY_DICT = \
{
'SmbExploiter': 'SMB Exploiter',
'WmiExploiter': 'WMI Exploiter',
'SSHExploiter': 'SSH Exploiter',
'RdpExploiter': 'RDP Exploiter',
'SambaCryExploiter': 'SambaCry Exploiter',
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter',
'ShellShockExploiter': 'ShellShock Exploiter',
}
class ISSUES_DICT(Enum):
WEAK_PASSWORD = 0
STOLEN_CREDS = 1
ELASTIC = 2
SAMBACRY = 3
SHELLSHOCK = 4
CONFICKER = 5
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
TUNNEL = 1
@staticmethod
def get_first_monkey_time():
return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', 1)]).limit(1)[0]['timestamp']
@staticmethod
def get_last_monkey_dead_time():
return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', -1)]).limit(1)[0]['timestamp']
@staticmethod
def get_monkey_duration():
delta = ReportService.get_last_monkey_dead_time() - ReportService.get_first_monkey_time()
st = ""
hours, rem = divmod(delta.seconds, 60 * 60)
minutes, seconds = divmod(rem, 60)
if delta.days > 0:
st += "%d days, " % delta.days
if hours > 0:
st += "%d hours, " % hours
st += "%d minutes and %d seconds" % (minutes, seconds)
return st
@staticmethod
def get_tunnels():
return [
{
'type': 'tunnel',
'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['_id'])),
'dest': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['tunnel']))
}
for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})]
@staticmethod
def get_scanned():
nodes = \
[NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \
+ [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
mongo.db.monkey.find({}, {'_id': 1})]
nodes = [
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes':
(x['hostname'] for x in
(NodeService.get_displayed_node_by_id(edge['from'], True)
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))),
'services': node['services']
}
for node in nodes]
return nodes
@staticmethod
def get_exploited():
exploited = \
[NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
mongo.db.monkey.find({}, {'_id': 1})
if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] \
+ [NodeService.get_displayed_node_by_id(node['_id'], True)
for node in mongo.db.node.find({'exploited': True}, {'_id': 1})]
exploited = [
{
'label': monkey['label'],
'ip_addresses': monkey['ip_addresses'],
'exploits': list(set(
[ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if
exploit['result']]))
}
for monkey in exploited]
return exploited
@staticmethod
def get_stolen_creds():
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
creds = []
for telem in mongo.db.telemetry.find(
{'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}},
{'data.credentials': 1, 'monkey_guid': 1}
):
monkey_creds = telem['data']['credentials']
if len(monkey_creds) == 0:
continue
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
for user in monkey_creds:
for pass_type in monkey_creds[user]:
creds.append(
{
'username': user.replace(',', '.'),
'type': PASS_TYPE_DICT[pass_type],
'origin': origin
}
)
return creds
@staticmethod
def process_general_exploit(exploit):
ip_addr = exploit['data']['machine']['ip_addr']
return {'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_ip(ip_addr)),
'ip_address': ip_addr}
@staticmethod
def process_general_creds_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
for attempt in exploit['data']['attempts']:
if attempt['result']:
processed_exploit['username'] = attempt['user']
if len(attempt['password']) > 0:
processed_exploit['type'] = 'password'
processed_exploit['password'] = attempt['password']
else:
processed_exploit['type'] = 'hash'
return processed_exploit
@staticmethod
def process_smb_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
if processed_exploit['type'] == 'password':
processed_exploit['type'] = 'smb_password'
else:
processed_exploit['type'] = 'smb_pth'
return processed_exploit
@staticmethod
def process_wmi_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
if processed_exploit['type'] == 'password':
processed_exploit['type'] = 'wmi_password'
else:
processed_exploit['type'] = 'wmi_pth'
return processed_exploit
@staticmethod
def process_ssh_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'ssh'
return processed_exploit
@staticmethod
def process_rdp_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'rdp'
return processed_exploit
@staticmethod
def process_sambacry_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'sambacry'
return processed_exploit
@staticmethod
def process_elastic_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'elastic'
return processed_exploit
@staticmethod
def process_conficker_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'conficker'
return processed_exploit
@staticmethod
def process_shellshock_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'shellshock'
urls = exploit['data']['info']['vulnerable_urls']
processed_exploit['port'] = urls[0].split(':')[2].split('/')[0]
processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls]
return processed_exploit
@staticmethod
def process_exploit(exploit):
exploiter_type = exploit['data']['exploiter']
EXPLOIT_PROCESS_FUNCTION_DICT = {
'SmbExploiter': ReportService.process_smb_exploit,
'WmiExploiter': ReportService.process_wmi_exploit,
'SSHExploiter': ReportService.process_ssh_exploit,
'RdpExploiter': ReportService.process_rdp_exploit,
'SambaCryExploiter': ReportService.process_sambacry_exploit,
'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit,
'ShellShockExploiter': ReportService.process_shellshock_exploit,
}
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@staticmethod
def get_exploits():
exploits = []
for exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True}):
new_exploit = ReportService.process_exploit(exploit)
if new_exploit not in exploits:
exploits.append(new_exploit)
return exploits
@staticmethod
def get_monkey_subnets(monkey_guid):
network_info = mongo.db.telemetry.find_one(
{'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid},
{'data.network_info.networks': 1}
)
if network_info is None:
return []
return \
[
ipaddress.ip_interface(unicode(network['addr'] + '/' + network['netmask'])).network
for network in network_info['data']['network_info']['networks']
]
@staticmethod
def get_cross_segment_issues():
issues = []
island_ips = local_ip_addresses()
for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}):
found_good_ip = False
monkey_subnets = ReportService.get_monkey_subnets(monkey['guid'])
for subnet in monkey_subnets:
for ip in island_ips:
if ipaddress.ip_address(unicode(ip)) in subnet:
found_good_ip = True
break
if found_good_ip:
break
if not found_good_ip:
issues.append(
{'type': 'cross_segment', 'machine': monkey['hostname'],
'networks': [str(subnet) for subnet in monkey_subnets],
'server_networks': [str(subnet) for subnet in get_subnets()]}
)
return issues
@staticmethod
def get_issues():
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues()
issues_dict = {}
for issue in issues:
machine = issue['machine']
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
return issues_dict
@staticmethod
def get_manual_monkeys():
return [monkey['hostname'] for monkey in mongo.db.monkey.find({}, {'hostname': 1, 'parent': 1, 'guid': 1}) if
NodeService.get_monkey_manual_run(monkey)]
@staticmethod
def get_config_users():
return ConfigService.get_config_value(['basic', 'credentials', 'exploit_user_list'], True)
@staticmethod
def get_config_passwords():
return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list'], True)
@staticmethod
def get_config_exploits():
exploits_config_value = ['exploits', 'general', 'exploiter_classes']
default_exploits = ConfigService.get_default_config()
for namespace in exploits_config_value:
default_exploits = default_exploits[namespace]
exploits = ConfigService.get_config_value(exploits_config_value, True)
if exploits == default_exploits:
return ['default']
return [ReportService.EXPLOIT_DISPLAY_DICT[exploit] for exploit in
exploits]
@staticmethod
def get_config_ips():
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True) != 'FixedRange':
return []
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True)
@staticmethod
def get_config_scan():
return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True)
@staticmethod
def get_issues_overview(issues, config_users, config_passwords):
issues_byte_array = [False] * 6
for machine in issues:
for issue in issues[machine]:
if issue['type'] == 'elastic':
issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True
elif issue['type'] == 'sambacry':
issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True
elif issue['type'] == 'shellshock':
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
elif issue['type'] == 'conficker':
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
issue['username'] in config_users:
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
return issues_byte_array
@staticmethod
def get_warnings_overview(issues):
warnings_byte_array = [False] * 2
for machine in issues:
for issue in issues[machine]:
if issue['type'] == 'cross_segment':
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
elif issue['type'] == 'tunnel':
warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True
return warnings_byte_array
@staticmethod
def is_report_generated():
generated_report = mongo.db.report.find_one({'name': 'generated_report'})
if generated_report is None:
return False
return generated_report['value']
@staticmethod
def set_report_generated():
mongo.db.report.update(
{'name': 'generated_report'},
{'$set': {'value': True}},
upsert=True)
@staticmethod
def get_report():
issues = ReportService.get_issues()
config_users = ReportService.get_config_users()
config_passwords = ReportService.get_config_passwords()
report = \
{
'overview':
{
'manual_monkeys': ReportService.get_manual_monkeys(),
'config_users': config_users,
'config_passwords': config_passwords,
'config_exploits': ReportService.get_config_exploits(),
'config_ips': ReportService.get_config_ips(),
'config_scan': ReportService.get_config_scan(),
'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"),
'monkey_duration': ReportService.get_monkey_duration(),
'issues': ReportService.get_issues_overview(issues, config_users, config_passwords),
'warnings': ReportService.get_warnings_overview(issues)
},
'glance':
{
'scanned': ReportService.get_scanned(),
'exploited': ReportService.get_exploited(),
'stolen_creds': ReportService.get_stolen_creds()
},
'recommendations':
{
'issues': issues
}
}
finished_run = NodeService.is_monkey_finished_running()
if finished_run:
ReportService.set_report_generated()
return report
@staticmethod
def did_exploit_type_succeed(exploit_type):
return mongo.db.edge.count(
{'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}},
limit=1) > 0

View File

@ -67,6 +67,7 @@
"js-file-download": "^0.4.1",
"normalize.css": "^4.0.0",
"prop-types": "^15.5.10",
"rc-progress": "^2.2.5",
"react": "^15.6.1",
"react-bootstrap": "^0.31.2",
"react-copy-to-clipboard": "^5.0.0",
@ -80,6 +81,7 @@
"react-modal-dialog": "^4.0.7",
"react-redux": "^5.0.6",
"react-router-dom": "^4.2.2",
"react-table": "^6.7.4",
"react-toggle": "^4.0.1",
"redux": "^3.7.2"
}

View File

@ -16,8 +16,10 @@ require('normalize.css/normalize.css');
require('react-data-components/css/table-twbs.css');
require('styles/App.css');
require('react-toggle/style.css');
require('react-table/react-table.css');
let logoImage = require('../images/monkey-logo.png');
let logoImage = require('../images/monkey-icon.svg');
let infectionMonkeyImage = require('../images/infection-monkey.svg');
let guardicoreLogoImage = require('../images/guardicore-logo.png');
class AppComponent extends React.Component {
@ -27,7 +29,8 @@ class AppComponent extends React.Component {
completedSteps: {
run_server: true,
run_monkey: false,
infection_done: false
infection_done: false,
report_done: false
}
};
}
@ -66,7 +69,8 @@ class AppComponent extends React.Component {
<Row>
<Col sm={3} md={2} className="sidebar">
<div className="header">
<img src={logoImage} alt="Infection Monkey"/>
<img src={logoImage} style={{width: '5vw', margin: '15px'}}/>
<img src={infectionMonkeyImage} style={{width: '15vw'}} alt="Infection Monkey"/>
</div>
<ul className="navigation">
@ -101,6 +105,9 @@ class AppComponent extends React.Component {
<NavLink to="/report">
<span className="number">4.</span>
Security Report
{ this.state.completedSteps.report_done ?
<Icon name="check" className="pull-right checkmark text-success"/>
: ''}
</NavLink>
</li>
<li>

View File

@ -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';
}

View File

@ -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) {
@ -53,7 +15,9 @@ class MapPageComponent extends React.Component {
selected: null,
selectedType: null,
killPressed: false,
showKillDialog: false
showKillDialog: false,
telemetry: [],
telemetryLastTimestamp: null
};
}
@ -61,41 +25,47 @@ 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);
this.interval = setInterval(this.timedEvents, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
timedEvents = () => {
this.updateMapFromServer();
this.updateTelemetryFromServer();
};
updateMapFromServer = () => {
fetch('/api/netmap')
.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();
});
};
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 +126,26 @@ class MapPageComponent extends React.Component {
)
};
renderTelemetryEntry(telemetry) {
return (
<div key={telemetry.id}>
<span className="date">{telemetry.timestamp}</span>
<span className="source"> {telemetry.hostname}:</span>
<span className="event"> {telemetry.brief}</span>
</div>
);
}
renderTelemetryConsole() {
return (
<div className="telemetry-console">
{
this.state.telemetry.map(this.renderTelemetryEntry)
}
</div>
);
}
render() {
return (
<div>
@ -174,17 +164,7 @@ class MapPageComponent extends React.Component {
<b style={{color: '#aeaeae'}}> | </b>
<span>Island Communication <i className="fa fa-lg fa-minus" style={{color: '#a9aaa9'}} /></span>
</div>
{
/*
<div className="telemetry-console">
<div>
<span className="date">2017-10-16 16:00:05</span>
<span className="source"> monkey-elastic</span>
<span className="event"> bla bla</span>
</div>
</div>
*/
}
{ this.renderTelemetryConsole() }
<div style={{height: '80vh'}}>
<ReactiveGraph graph={this.state.graph} options={options} events={this.events}/>
</div>

View File

@ -1,23 +1,692 @@
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';
import {edgeGroupToColor, options} from 'components/map/MapOptions';
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');
let monkeyLogoImage = require('../../images/monkey-icon.svg');
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.state = {
report: {},
graph: {nodes: [], edges: []},
allMonkeysAreDead: false,
runStarted: true
};
}
componentDidMount() {
this.updateMonkeysRunning().then(res => this.getReportFromServer(res));
this.updateMapFromServer();
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 = (<h1>Generating Report...</h1>);
} else {
content =
<p className="alert alert-warning">
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
You have to run a monkey before generating a report!
</p>;
}
} else {
content = this.generateReportContent();
}
return (
<Col xs={12} lg={8}>
<h1 className="page-title">4. Security Report</h1>
<h1 className="page-title no-print">4. Security Report</h1>
<div style={{'fontSize': '1.2em'}}>
<p>
Under construction
</p>
{content}
</div>
</Col>
);
}
updateMonkeysRunning = () => {
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']),
runStarted: res['completed_steps']['run_monkey']
});
return res;
});
};
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(res) {
if (res['completed_steps']['run_monkey']) {
fetch('/api/report')
.then(res => res.json())
.then(res => {
this.setState({
report: res
});
});
}
}
generateReportContent() {
return (
<div>
<div className="text-center no-print" style={{marginBottom: '20px'}}>
<Button bsSize="large" onClick={() => {
print();
}}><i className="glyphicon glyphicon-print"/> Print Report</Button>
</div>
<div className="report-page">
{this.generateReportHeader()}
<hr/>
{this.generateReportOverviewSection()}
{this.generateReportFindingsSection()}
{this.generateReportRecommendationsSection()}
{this.generateReportGlanceSection()}
{this.generateReportFooter()}
</div>
<div className="text-center no-print" style={{marginTop: '20px'}}>
<Button bsSize="large" onClick={() => {
print();
}}><i className="glyphicon glyphicon-print"/> Print Report</Button>
</div>
</div>
);
}
generateReportHeader() {
return (
<div id="header" className="row justify-content-between">
<Col xs={8}>
<div>
<h1 style={{marginTop: '0px', marginBottom: '5px', color: '#666666', fontFamily: 'Alegreya'}}>Security Report</h1>
<h1 style={{marginTop: '0px', marginBottom: '0px', color: '#ffcc00', fontFamily: 'Alegreya'}}>Infection <b>Monkey</b></h1>
</div>
</Col>
<Col xs={4}>
<img src={monkeyLogoImage}
style={{
float: 'right',
width: '80px'
}}/>
</Col>
</div>
);
}
generateReportOverviewSection() {
return (
<div id="overview">
<h2>
Overview
</h2>
{
this.state.report.glance.exploited.length > 0 ?
(<p className="alert alert-danger">
<i className="glyphicon glyphicon-exclamation-sign" style={{'marginRight': '5px'}}/>
Critical security issues were detected!
</p>) :
(<p className="alert alert-success">
<i className="glyphicon glyphicon-ok-sign" style={{'marginRight': '5px'}}/>
No critical security issues were detected.
</p>)
}
{
this.state.allMonkeysAreDead ?
''
:
(<p className="alert alert-warning">
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
Some monkeys are still running. To get the best report it's best to wait for all of them to finish
running.
</p>)
}
{
this.state.report.glance.exploited.length > 0 ?
''
:
<p className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
To improve the monkey's detection rates, try adding users and passwords and enable the "Local
network
scan" config value under <b>Basic - Network</b>.
</p>
}
<p>
The first monkey run was started on <span
className="label label-info">{this.state.report.overview.monkey_start_time}</span>. After <span
className="label label-info">{this.state.report.overview.monkey_duration}</span>, all monkeys finished
propagation attempts.
</p>
<p>
The monkey started propagating from the following machines where it was manually installed:
<ul>
{this.state.report.overview.manual_monkeys.map(x => <li>{x}</li>)}
</ul>
</p>
<p>
The monkeys were run with the following configuration:
</p>
{
this.state.report.overview.config_users.length > 0 ?
<p>
Usernames used for brute-forcing:
<ul>
{this.state.report.overview.config_users.map(x => <li>{x}</li>)}
</ul>
Passwords used for brute-forcing:
<ul>
{this.state.report.overview.config_passwords.map(x => <li>{x.substr(0, 3) + '******'}</li>)}
</ul>
</p>
:
<p>
Brute forcing uses stolen credentials only. No credentials were supplied during Monkeys
configuration.
</p>
}
{
this.state.report.overview.config_exploits.length > 0 ?
(
this.state.report.overview.config_exploits[0] === 'default' ?
''
:
<p>
The Monkey uses the following exploit methods:
<ul>
{this.state.report.overview.config_exploits.map(x => <li>{x}</li>)}
</ul>
</p>
)
:
<p>
No exploits are used by the Monkey.
</p>
}
{
this.state.report.overview.config_ips.length > 0 ?
<p>
The Monkey scans the following IPs:
<ul>
{this.state.report.overview.config_ips.map(x => <li>{x}</li>)}
</ul>
</p>
:
''
}
{
this.state.report.overview.config_scan ?
''
:
<p>
Note: Monkeys were configured to avoid scanning of the local network.
</p>
}
</div>
);
}
generateReportFindingsSection() {
return (
<div id="findings">
<h3>
Security Findings
</h3>
<div>
<h3>
Immediate Threats
</h3>
{
this.state.report.overview.issues.filter(function (x) {
return x === true;
}).length > 0 ?
<div>
During this simulated attack the Monkey uncovered <span
className="label label-warning">
{this.state.report.overview.issues.filter(function (x) {
return x === true;
}).length} threats</span>:
<ul>
{this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
<li>Stolen credentials are used to exploit other machines.</li> : null}
{this.state.report.overview.issues[this.Issue.ELASTIC] ?
<li>Elasticsearch servers are vulnerable to <a
href="https://www.cvedetails.com/cve/cve-2015-1427">CVE-2015-1427</a>.
</li> : null}
{this.state.report.overview.issues[this.Issue.SAMBACRY] ?
<li>Samba servers are vulnerable to SambaCry (<a
href="https://www.samba.org/samba/security/CVE-2017-7494.html"
>CVE-2017-7494</a>).</li> : null}
{this.state.report.overview.issues[this.Issue.SHELLSHOCK] ?
<li>Machines are vulnerable to Shellshock (<a
href="https://www.cvedetails.com/cve/CVE-2014-6271">CVE-2014-6271</a>).
</li> : null}
{this.state.report.overview.issues[this.Issue.CONFICKER] ?
<li>Machines are vulnerable to Conficker (<a
href="https://docs.microsoft.com/en-us/security-updates/SecurityBulletins/2008/ms08-067"
>MS08-067</a>).</li> : null}
{this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
<li>Machines are accessible using passwords supplied by the user during the Monkeys
configuration.</li> : null}
</ul>
</div>
:
<div>
During this simulated attack the Monkey uncovered <span
className="label label-success">0 threats</span>.
</div>
}
</div>
<div>
<h3>
Potential Security Issues
</h3>
{
this.state.report.overview.warnings.filter(function (x) {
return x === true;
}).length > 0 ?
<div>
The Monkey uncovered the following possible set of issues:
<ul>
{this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ?
<li>Weak segmentation - Machines from different segments are able to
communicate.</li> : null}
{this.state.report.overview.warnings[this.Warning.TUNNEL] ?
<li>Weak segmentation - machines were able to communicate over unused ports.</li> : null}
</ul>
</div>
:
<div>
The Monkey did not find any issues.
</div>
}
</div>
</div>
);
}
generateReportRecommendationsSection() {
return (
<div id="recommendations">
<h3>
Recommendations
</h3>
<div>
{this.generateIssues(this.state.report.recommendations.issues)}
</div>
</div>
);
}
generateReportGlanceSection() {
let exploitPercentage =
(100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length;
return (
<div id="glance">
<h3>
The Network from the Monkey's Eyes
</h3>
<div>
<p>
The Monkey discovered <span
className="label label-warning">{this.state.report.glance.scanned.length}</span> machines and
successfully breached <span
className="label label-danger">{this.state.report.glance.exploited.length}</span> of them.
</p>
<div className="text-center" style={{margin: '10px'}}>
<Line style={{width: '300px', marginRight: '5px'}} percent={exploitPercentage} strokeWidth="4"
trailWidth="4"
strokeColor="#d9534f" trailColor="#f0ad4e"/>
<b>{Math.round(exploitPercentage)}% of scanned machines exploited</b>
</div>
</div>
<p>
From the attacker's point of view, the network looks like this:
</p>
<div className="map-legend">
<b>Legend: </b>
<span>Exploit <i className="fa fa-lg fa-minus" style={{color: '#cc0200'}}/></span>
<b style={{color: '#aeaeae'}}> | </b>
<span>Scan <i className="fa fa-lg fa-minus" style={{color: '#ff9900'}}/></span>
<b style={{color: '#aeaeae'}}> | </b>
<span>Tunnel <i className="fa fa-lg fa-minus" style={{color: '#0158aa'}}/></span>
<b style={{color: '#aeaeae'}}> | </b>
<span>Island Communication <i className="fa fa-lg fa-minus" style={{color: '#a9aaa9'}}/></span>
</div>
<div style={{position: 'relative', height: '80vh'}}>
<ReactiveGraph graph={this.state.graph} options={options}/>
</div>
<div style={{marginBottom: '20px'}}>
<BreachedServers data={this.state.report.glance.exploited}/>
</div>
<div style={{marginBottom: '20px'}}>
<ScannedServers data={this.state.report.glance.scanned}/>
</div>
<div>
<StolenPasswords data={this.state.report.glance.stolen_creds}/>
</div>
</div>
);
}
generateReportFooter() {
return (
<div id="footer" className="text-center" style={{marginTop: '20px'}}>
For questions, suggestions or any other feedback
contact: <a href="mailto://labs@guardicore.com" className="no-print">labs@guardicore.com</a>
<div className="force-print" style={{display: 'none'}}>labs@guardicore.com</div>
<img src={guardicoreLogoImage} alt="GuardiCore" className="center-block" style={{height: '50px'}}/>
</div>
);
}
generateInfoBadges(data_array) {
return data_array.map(badge_data => <span className="label label-info" style={{margin: '2px'}}>{badge_data}</span>);
}
generateShellshockPathListBadges(paths) {
return paths.map(path => <span className="label label-warning" style={{margin: '2px'}}>{path}</span>);
}
generateSmbPasswordIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">SMB</span> attack.
<br/>
The Monkey authenticated over the SMB protocol with user <span
className="label label-success">{issue.username}</span> and its password.
</CollapsibleWellComponent>
</li>
);
}
generateSmbPthIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">SMB</span> attack.
<br/>
The Monkey used a pass-the-hash attack over SMB protocol with user <span
className="label label-success">{issue.username}</span>.
</CollapsibleWellComponent>
</li>
);
}
generateWmiPasswordIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">WMI</span> attack.
<br/>
The Monkey authenticated over the WMI protocol with user <span
className="label label-success">{issue.username}</span> and its password.
</CollapsibleWellComponent>
</li>
);
}
generateWmiPthIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">WMI</span> attack.
<br/>
The Monkey used a pass-the-hash attack over WMI protocol with user <span
className="label label-success">{issue.username}</span>.
</CollapsibleWellComponent>
</li>
);
}
generateSshIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">SSH</span> attack.
<br/>
The Monkey authenticated over the SSH protocol with user <span
className="label label-success">{issue.username}</span> and its password.
</CollapsibleWellComponent>
</li>
);
}
generateRdpIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">RDP</span> attack.
<br/>
The Monkey authenticated over the RDP protocol with user <span
className="label label-success">{issue.username}</span> and its password.
</CollapsibleWellComponent>
</li>
);
}
generateSambaCryIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<br/>
Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">SambaCry</span> attack.
<br/>
The Monkey authenticated over the SMB protocol with user <span
className="label label-success">{issue.username}</span> and its password, and used the SambaCry
vulnerability.
</CollapsibleWellComponent>
</li>
);
}
generateElasticIssue(issue) {
return (
<li>
Update your Elastic Search server to version 1.4.3 and up.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to an <span
className="label label-danger">Elastic Groovy</span> attack.
<br/>
The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.
</CollapsibleWellComponent>
</li>
);
}
generateShellshockIssue(issue) {
return (
<li>
Update your Bash to a ShellShock-patched version.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">ShellShock</span> attack.
<br/>
The attack was made possible because the HTTP server running on TCP port <span
className="label label-info">{issue.port}</span> was vulnerable to a shell injection attack on the
paths: {this.generateShellshockPathListBadges(issue.paths)}.
</CollapsibleWellComponent>
</li>
);
}
generateConfickerIssue(issue) {
return (
<li>
Install the latest Windows updates or upgrade to a newer operating system.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">Conficker</span> attack.
<br/>
The attack was made possible because the target machine used an outdated and unpatched operating system
vulnerable to Conficker.
</CollapsibleWellComponent>
</li>
);
}
generateCrossSegmentIssue(issue) {
return (
<li>
Segment your network and make sure there is no communication between machines from different segments.
<CollapsibleWellComponent>
The network can probably be segmented. A monkey instance on <span
className="label label-primary">{issue.machine}</span> in the
networks {this.generateInfoBadges(issue.networks)}
could directly access the Monkey Island C&C server in the
networks {this.generateInfoBadges(issue.server_networks)}.
</CollapsibleWellComponent>
</li>
);
}
generateTunnelIssue(issue) {
return (
<li>
Use micro-segmentation policies to disable communication other than the required.
<CollapsibleWellComponent>
Machines are not locked down at port level. Network tunnel was set up from <span
className="label label-primary">{issue.machine}</span> to <span
className="label label-primary">{issue.dest}</span>.
</CollapsibleWellComponent>
</li>
);
}
generateIssue = (issue) => {
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 data;
};
generateIssues = (issues) => {
let issuesDivArray = [];
for (let machine of Object.keys(issues)) {
issuesDivArray.push(
<li>
<h4><b>{machine}</b></h4>
<ol>
{issues[machine].map(this.generateIssue)}
</ol>
</li>
);
}
return <ul>{issuesDivArray}</ul>;
};
}
export default ReportPageComponent;

View File

@ -0,0 +1,45 @@
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: '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;
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 (
<div className="data-table-container">
<ReactTable
columns={columns}
data={this.props.data}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
);
}
}
export default BreachedServersComponent;

View File

@ -0,0 +1,40 @@
import React from 'react';
import {Button, Collapse, Well} from 'react-bootstrap';
class CollapsibleWellComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false
};
}
render() {
let well =
(
<Well style={{margin: '10px'}}>
{this.props.children}
</Well>
);
return (
<div>
<div className="no-print">
<Button onClick={() => this.setState({open: !this.state.open})} bsStyle="link">
Read More...
</Button>
<Collapse in={this.state.open}>
<div>
{well}
</div>
</Collapse>
</div>
<div className="force-print" style={{display: 'none'}}>
{well}
</div>
</div>
);
}
}
export default CollapsibleWellComponent;

View File

@ -0,0 +1,46 @@
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: '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;
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 (
<div className="data-table-container">
<ReactTable
columns={columns}
data={this.props.data}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
);
}
}
export default ScannedServersComponent;

View File

@ -0,0 +1,38 @@
import React from 'react';
import ReactTable from 'react-table'
const columns = [
{
Header: 'Stolen Credentials',
columns: [
{ Header: 'Username', accessor: 'username'},
{ Header: 'Type', accessor: 'type'},
{ Header: 'Stolen From', 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 (
<div className="data-table-container">
<ReactTable
columns={columns}
data={this.props.data}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
/>
</div>
);
}
}
export default StolenPasswordsComponent;

View File

@ -0,0 +1,32 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 97.577 11.46">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<title>14cbedff-3eed-4f8f-abb7-fffe92867ded</title>
<g>
<path class="cls-1" d="M89.22,6.874l1.819-.159a3.153,3.153,0,1,1,.124-1.065,2.306,2.306,0,0,1-.005.492l-4.489.393a1.257,1.257,0,0,0,1.414.989,1.417,1.417,0,0,0,1.06-.56A.6.6,0,0,1,89.22,6.874Zm.087-1.767a1.311,1.311,0,0,0-1.443-.916,1.282,1.282,0,0,0-1.249,1.151Z"/>
<path class="cls-1" d="M67.715,4.191a1.622,1.622,0,1,1-1.622,1.622,1.624,1.624,0,0,1,1.622-1.622m0-1.542a3.164,3.164,0,1,0,3.164,3.164A3.164,3.164,0,0,0,67.715,2.65Z"/>
<path class="cls-1" d="M54.893,8.808,55.53.648A6.958,6.958,0,0,1,56.736.564a6.743,6.743,0,0,1,1.193.084L58.7,3.7q.324,1.332.456,2.148h.071q.132-.816.456-2.148L60.45.648a6.914,6.914,0,0,1,1.2-.084,6.7,6.7,0,0,1,1.188.084l.636,8.16a3.218,3.218,0,0,1-.863.1,5.125,5.125,0,0,1-.864-.06l-.192-3.48q-.1-1.656-.1-2.724h-.084L60.126,7.62a5.447,5.447,0,0,1-.93.06,5.9,5.9,0,0,1-.954-.06L57.006,2.64h-.084q-.023,1.464-.1,2.724l-.192,3.48a5.125,5.125,0,0,1-.864.06A3.366,3.366,0,0,1,54.893,8.808Z"/>
<path class="cls-1" d="M77.094,4.86v2.5a2.089,2.089,0,0,0,.288,1.188,1.611,1.611,0,0,1-1.05.384.983.983,0,0,1-.8-.276,1.321,1.321,0,0,1-.223-.84V5.2a1.33,1.33,0,0,0-.12-.648.446.446,0,0,0-.42-.2,1.386,1.386,0,0,0-.983.468v4.02A4.716,4.716,0,0,1,72.9,8.9a5.113,5.113,0,0,1-.906-.072V2.856l.084-.084h.672a.9.9,0,0,1,.948.72,2.669,2.669,0,0,1,1.7-.744,1.477,1.477,0,0,1,1.26.576A2.5,2.5,0,0,1,77.094,4.86Z"/>
<path class="cls-1" d="M81.3,6.408l-.4.012V8.832a4.606,4.606,0,0,1-.864.072,5,5,0,0,1-.888-.072V.084L79.241,0h.7a.973.973,0,0,1,.75.246,1.243,1.243,0,0,1,.222.834V4.944h.216a.369.369,0,0,0,.336-.216l.685-1.26a1.062,1.062,0,0,1,1.008-.66q.347,0,1,.024l.071.1-.972,1.824a1.885,1.885,0,0,1-.6.672,1.7,1.7,0,0,1,1.02,1.044l.3.888a4.932,4.932,0,0,0,.307.708.941.941,0,0,0,.27.3,1.512,1.512,0,0,1-1.2.564.839.839,0,0,1-.582-.18,1.534,1.534,0,0,1-.354-.636l-.36-1.056a1.225,1.225,0,0,0-.306-.516A.675.675,0,0,0,81.3,6.408Z"/>
<path class="cls-1" d="M97.577,3.036l-1.5,5.628a5.042,5.042,0,0,1-1,2.1,2.437,2.437,0,0,1-1.9.7,4.322,4.322,0,0,1-1.416-.24V11a2.268,2.268,0,0,1,.09-.51,1.213,1.213,0,0,1,.27-.54,3.946,3.946,0,0,0,1.057.18,1.231,1.231,0,0,0,1.212-1.044l.048-.168q-1.008-.024-1.2-.744L91.805,2.988a2.4,2.4,0,0,1,.989-.24,1.049,1.049,0,0,1,.666.168,1.108,1.108,0,0,1,.313.6l.6,2.376q.1.348.288,1.6a.094.094,0,0,0,.108.084l1.08-4.716a3.485,3.485,0,0,1,.786-.072,2.786,2.786,0,0,1,.882.132Z"/>
</g>
<g>
<path class="cls-1" d="M0,8.937V.741A1.882,1.882,0,0,1,.414.705,2.229,2.229,0,0,1,.852.741v8.2a2.231,2.231,0,0,1-.432.036A2.056,2.056,0,0,1,0,8.937Z"/>
<path class="cls-1" d="M8.232,4.809V7.833a2.871,2.871,0,0,0,.12,1,.7.7,0,0,1-.517.18q-.42,0-.42-.54V5.085a2.2,2.2,0,0,0-.191-1.074.747.747,0,0,0-.7-.318,2.332,2.332,0,0,0-1.044.27,2.7,2.7,0,0,0-.888.69V8.937a1.994,1.994,0,0,1-.408.036,1.987,1.987,0,0,1-.408-.036V3.093l.072-.072h.312a.384.384,0,0,1,.348.132.831.831,0,0,1,.085.432v.24a2.983,2.983,0,0,1,.966-.636A2.908,2.908,0,0,1,6.7,2.949a1.335,1.335,0,0,1,1.17.516A2.272,2.272,0,0,1,8.232,4.809Z"/>
<path class="cls-1" d="M11.147,8.937V3.669l-1.031.012a1.473,1.473,0,0,1-.036-.312,1.639,1.639,0,0,1,.036-.324l1,.024q-.085-1.115-.084-1.272A1.769,1.769,0,0,1,11.472.555,1.538,1.538,0,0,1,12.666.069a3.728,3.728,0,0,1,1.386.252.818.818,0,0,1-.2.564A3.881,3.881,0,0,0,12.756.693a.883.883,0,0,0-.708.294,1.226,1.226,0,0,0-.252.822,9.977,9.977,0,0,0,.1,1.26l1.776-.024a1.716,1.716,0,0,1,.036.324,1.54,1.54,0,0,1-.036.312l-1.716-.012V8.937a2.258,2.258,0,0,1-.8,0Z"/>
<path class="cls-1" d="M32.352,3.045a1.716,1.716,0,0,1,.036.324,1.54,1.54,0,0,1-.036.312l-1.524-.012v4.14q0,.492.468.492h.925a1,1,0,0,1,.071.372,1.405,1.405,0,0,1-.012.24,8.879,8.879,0,0,1-1.122.072,1.268,1.268,0,0,1-.846-.246.87.87,0,0,1-.288-.7V3.669l-.828.012a1.336,1.336,0,0,1-.036-.306,1.664,1.664,0,0,1,.036-.33l.828.012v-1.3a.794.794,0,0,1,.09-.432.4.4,0,0,1,.354-.132h.288l.072.072v1.8Z"/>
<path class="cls-1" d="M35.472,3.837v5.1a2.324,2.324,0,0,1-.816,0V4.257a.7.7,0,0,0-.108-.456.5.5,0,0,0-.384-.12h-.107a1.288,1.288,0,0,1-.036-.294,1.616,1.616,0,0,1,.036-.318,4.4,4.4,0,0,1,.575-.048h.1a.7.7,0,0,1,.546.216A.856.856,0,0,1,35.472,3.837ZM34.379,1.881a.986.986,0,0,1-.084-.438.837.837,0,0,1,.084-.414.853.853,0,0,1,.511-.12.8.8,0,0,1,.5.12.837.837,0,0,1,.084.414.986.986,0,0,1-.084.438A.707.707,0,0,1,34.9,2,.874.874,0,0,1,34.379,1.881Z"/>
<path class="cls-1" d="M49.139,4.809V7.833a2.871,2.871,0,0,0,.12,1,.7.7,0,0,1-.517.18q-.42,0-.42-.54V5.085a2.2,2.2,0,0,0-.191-1.074.747.747,0,0,0-.7-.318,2.332,2.332,0,0,0-1.044.27,2.7,2.7,0,0,0-.888.69V8.937a2.324,2.324,0,0,1-.816,0V3.093l.072-.072h.312a.384.384,0,0,1,.348.132.831.831,0,0,1,.085.432v.24a2.983,2.983,0,0,1,.966-.636,2.908,2.908,0,0,1,1.134-.24,1.335,1.335,0,0,1,1.17.516A2.272,2.272,0,0,1,49.139,4.809Z"/>
<path class="cls-1" d="M40.075,3.555a2.364,2.364,0,1,1-2.364,2.364,2.367,2.367,0,0,1,2.364-2.364m0-.8a3.164,3.164,0,1,0,3.164,3.164,3.164,3.164,0,0,0-3.164-3.164Z"/>
<path class="cls-1" d="M25.315,8.285a2.349,2.349,0,0,1-2.04-1.18,2.335,2.335,0,0,1,0-2.36,2.344,2.344,0,0,1,4.08,0l.7-.4a3.16,3.16,0,1,0,0,3.16l-.7-.4A2.35,2.35,0,0,1,25.315,8.285Z"/>
<g>
<path class="cls-1" d="M17.87,8.285a2.365,2.365,0,1,1,2.36-2.37h.8a3.162,3.162,0,0,0-5.9-1.58,3.018,3.018,0,0,0-.43,1.58,3.165,3.165,0,0,0,5.91,1.58l-.69-.39A2.378,2.378,0,0,1,17.87,8.285Z"/>
<polygon class="cls-1" points="15.286 5.718 20.514 5.261 21.03 5.915 15.347 6.412 15.286 5.718"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -2,6 +2,7 @@
@import url('https://fonts.googleapis.com/css?family=Alegreya');
/* Base Application Styles */
body {
color: #fff;
background: #222;
@ -23,7 +24,7 @@ body {
left: 0;
z-index: 1000;
display: block;
padding: 1em;
padding: 0px !important;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
@ -34,6 +35,8 @@ body {
text-align: center;
font-weight: bold;
font-size: 1.2em;
background-color: #ffcc00;
padding: 15px;
}
.header img {
@ -42,11 +45,12 @@ body {
.navigation {
margin-top: 1em;
padding: 15px;
}
ul {
list-style: none;
padding-left: 0;
padding-left: 0px;
}
li {
@ -54,30 +58,29 @@ body {
}
li .number {
color: #666;
display: inline-block;
width: 1.1em;
}
li a {
/*color: #d30d09;*/
color: #666;
color: #333333;
display: block;
padding: 0.5em 1em;
margin: 0.1em 0;
}
li a:hover {
color: #000;
background: #e9e9e9;
text-decoration: none;
}
li a.active {
background: #fff;
background: #333333;
text-decoration: none;
color: #d30d09;
color: #ffcc00;
}
li a.active:hover {
color: #d30d09;
color: #ffcc00;
}
li a.disabled {
color: #666;
@ -92,6 +95,7 @@ body {
li .checkmark {
font-size: 1.3em;
margin-right: -10px;
color: #ffcc00;
}
hr {
@ -122,9 +126,9 @@ body {
margin-top: 0;
margin-bottom: 1em;
padding-bottom: 0.5em;
border-bottom: 2px dotted #d30d09;
border-bottom: 2px dotted #ffcc00;
font-size: 2.5em;
color: #d30d09;
color: #ffcc00;
font-family: 'Alegreya', serif;
}
@ -276,13 +280,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 {
@ -363,3 +368,123 @@ 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;
}
.no-print {
display: none;
}
.force-print {
display: block !important;
}
.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;
}
}

View File

@ -4,10 +4,9 @@ import sys
import array
import struct
import ipaddress
from netifaces import interfaces, ifaddresses, AF_INET
from cc.database import mongo
__author__ = 'Barak'
@ -56,3 +55,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

View File

@ -2,16 +2,17 @@
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
pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER
virtualenv -p python2.7 ${PYTHON_FOLDER}
# install pip requirements
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}

View File

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

View File

@ -10,3 +10,6 @@ Flask-Pymongo
Flask-Restful
jsonschema
netifaces
ipaddress
enum34
virtualenv

View File

@ -1,4 +1,4 @@
#!/bin/bash
cd /var/monkey_island/cc
python main.py
/var/monkey_island/bin/python/bin/python main.py

View File

@ -10,3 +10,5 @@ Flask-Pymongo
Flask-Restful
jsonschema
netifaces
ipaddress
enum34

View File

@ -0,0 +1 @@
xcopy %1 %2

View File

@ -1,3 +1,18 @@
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
@echo off
IF [%1] == [] (
set mydir=%cd%\
) ELSE (
set mydir=%~1%
)
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
"%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"

View File

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

View File

@ -0,0 +1 @@
del %1

View File

@ -1,3 +1,4 @@
if not exist db mkdir db
start windows\run_mongodb.bat
start windows\run_cc.bat
start https://localhost:5000