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/main.py b/chaos_monkey/main.py index 231734d56..c53232b2c 100644 --- a/chaos_monkey/main.py +++ b/chaos_monkey/main.py @@ -1,14 +1,17 @@ -import os -import sys -import logging -import traceback -import logging.config -from config import WormConfiguration, EXTERNAL_CONFIG_FILE -from model import MONKEY_ARG, DROPPER_ARG -from dropper import MonkeyDrops -from monkey import ChaosMonkey +from __future__ import print_function + import argparse import json +import logging +import logging.config +import os +import sys +import traceback + +from config import WormConfiguration, EXTERNAL_CONFIG_FILE +from dropper import MonkeyDrops +from model import MONKEY_ARG, DROPPER_ARG +from monkey import ChaosMonkey if __name__ == "__main__": sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -55,22 +58,22 @@ def main(): config_file = opts.config if os.path.isfile(config_file): # using print because config can also change log locations - print "Loading config from %s." % config_file + print("Loading config from %s." % config_file) try: with open(config_file) as config_fo: json_dict = json.load(config_fo) WormConfiguration.from_dict(json_dict) except ValueError as e: - print "Error loading config: %s, using default" % (e,) + print("Error loading config: %s, using default" % (e,)) else: print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,)) - print "Loaded Configuration: %r" % WormConfiguration.as_dict() + print("Loaded Configuration: %r" % WormConfiguration.as_dict()) # Make sure we're not in a machine that has the kill file kill_path = os.path.expandvars(WormConfiguration.kill_file_path_windows) if sys.platform == "win32" else WormConfiguration.kill_file_path_linux if os.path.exists(kill_path): - print "Kill path found, finished run" + print("Kill path found, finished run") return True try: diff --git a/chaos_monkey/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..7b37c3278 100644 --- a/chaos_monkey/network/tcp_scanner.py +++ b/chaos_monkey/network/tcp_scanner.py @@ -1,8 +1,7 @@ -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.tools import check_tcp_ports __author__ = 'itamar' @@ -17,29 +16,24 @@ 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: + ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.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 - is_open, banner = check_port_tcp(host.ip_addr, - target_port, - self._config.tcp_scan_timeout / 1000.0, - self._config.tcp_scan_get_banner) - - if is_open: - count += 1 - 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 66f4eef57..b26fe5d20 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -1,9 +1,11 @@ -import socket -import select import logging +import select +import socket import struct +import time DEFAULT_TIMEOUT = 10 +SLEEP_BETWEEN_POLL = 0.5 BANNER_READ = 1024 LOG = logging.getLogger(__name__) @@ -32,10 +34,18 @@ 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): + """ + 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) @@ -43,7 +53,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 @@ -54,26 +64,96 @@ def check_port_tcp(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() return True, banner -def check_port_udp(ip, port, timeout=DEFAULT_TIMEOUT): +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) - + 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() 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] + 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: + 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 + 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 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 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]))) + 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 port_attempts] + 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 [], [] diff --git a/chaos_monkey/system_info/__init__.py b/chaos_monkey/system_info/__init__.py index 0a5bf8e31..126854b8e 100644 --- a/chaos_monkey/system_info/__init__.py +++ b/chaos_monkey/system_info/__init__.py @@ -1,3 +1,4 @@ +import logging import socket import sys @@ -6,6 +7,8 @@ from enum import IntEnum from network.info import get_host_subnets +LOG = logging.getLogger(__name__) + # Linux doesn't have WindowsError try: WindowsError @@ -56,8 +59,9 @@ class InfoCollector(object): def get_hostname(self): """ Adds the fully qualified computer hostname to the system information. - :return: Nothing + :return: None. Updates class information """ + LOG.debug("Reading hostname") self.info['hostname'] = socket.getfqdn() def get_process_list(self): @@ -65,8 +69,9 @@ class InfoCollector(object): Adds process information from the host to the system information. Currently lists process name, ID, parent ID, command line and the full image path of each process. - :return: Nothing + :return: None. Updates class information """ + LOG.debug("Reading process list") processes = {} for process in psutil.process_iter(): try: @@ -95,6 +100,7 @@ class InfoCollector(object): Adds network information from the host to the system information. Currently updates with a list of networks accessible from host, containing host ip and the subnet range. - :return: None + :return: None. Updates class information """ + LOG.debug("Reading subnets") self.info['network_info'] = {'networks': get_host_subnets()} diff --git a/chaos_monkey/system_info/linux_info_collector.py b/chaos_monkey/system_info/linux_info_collector.py index 6c7570fc0..906173421 100644 --- a/chaos_monkey/system_info/linux_info_collector.py +++ b/chaos_monkey/system_info/linux_info_collector.py @@ -1,7 +1,11 @@ +import logging + from . import InfoCollector __author__ = 'uri' +LOG = logging.getLogger(__name__) + class LinuxInfoCollector(InfoCollector): """ @@ -12,6 +16,12 @@ class LinuxInfoCollector(InfoCollector): super(LinuxInfoCollector, self).__init__() def get_info(self): + """ + Collect Linux system information + Hostname, process list and network subnets + :return: Dict of system information + """ + LOG.debug("Running Linux collector") self.get_hostname() self.get_process_list() self.get_network_info() diff --git a/chaos_monkey/system_info/mimikatz_collector.py b/chaos_monkey/system_info/mimikatz_collector.py index 53f42ad4c..e69bcd73e 100644 --- a/chaos_monkey/system_info/mimikatz_collector.py +++ b/chaos_monkey/system_info/mimikatz_collector.py @@ -1,5 +1,5 @@ -import ctypes import binascii +import ctypes import logging import socket @@ -8,13 +8,14 @@ __author__ = 'itay.mizeretz' LOG = logging.getLogger(__name__) -class MimikatzCollector: +class MimikatzCollector(object): """ Password collection module for Windows using Mimikatz. """ def __init__(self): try: + self._isInit = False self._config = __import__('config').WormConfiguration self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) @@ -31,9 +32,9 @@ class MimikatzCollector: Gets the logon info from mimikatz. Returns a dictionary of users with their known credentials. """ - if not self._isInit: return {} + LOG.debug("Running mimikatz collector") try: entry_count = self._collect() diff --git a/chaos_monkey/system_info/windows_info_collector.py b/chaos_monkey/system_info/windows_info_collector.py index 2ba26fd34..72e189f81 100644 --- a/chaos_monkey/system_info/windows_info_collector.py +++ b/chaos_monkey/system_info/windows_info_collector.py @@ -1,5 +1,10 @@ -from . import InfoCollector +import logging + from mimikatz_collector import MimikatzCollector +from . import InfoCollector + +LOG = logging.getLogger(__name__) + __author__ = 'uri' @@ -12,6 +17,13 @@ class WindowsInfoCollector(InfoCollector): super(WindowsInfoCollector, self).__init__() def get_info(self): + """ + Collect Windows system information + Hostname, process list and network subnets + Tries to read credential secrets using mimikatz + :return: Dict of system information + """ + LOG.debug("Running Windows collector") self.get_hostname() self.get_process_list() self.get_network_info() diff --git a/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: diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 70001521a..9c85f6230 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.report import Report from cc.resources.root import Root +from cc.resources.telemetry_feed import TelemetryFeed from cc.services.config import ConfigService __author__ = 'Barak' @@ -89,5 +90,6 @@ def init_app(mongo_url): api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') api.add_resource(Report, '/api/report', '/api/report/') + api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') return app diff --git a/monkey_island/cc/resources/local_run.py b/monkey_island/cc/resources/local_run.py index 2fe7be3d1..3d18b49e6 100644 --- a/monkey_island/cc/resources/local_run.py +++ b/monkey_island/cc/resources/local_run.py @@ -36,7 +36,7 @@ def run_local_monkey(): # run the monkey try: - args = ["%s m0nk3y -s %s:%s" % (target_path, local_ip_addresses()[0], ISLAND_PORT)] + args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], ISLAND_PORT)] if sys.platform == "win32": args = "".join(args) pid = subprocess.Popen(args, shell=True).pid diff --git a/monkey_island/cc/resources/telemetry_feed.py b/monkey_island/cc/resources/telemetry_feed.py new file mode 100644 index 000000000..24e0dcc51 --- /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... + telemetries = mongo.db.telemetry.find({}) + else: + 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 + } diff --git a/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey_island/cc/ui/src/components/pages/MapPage.js index aa5468380..ba5a655b1 100644 --- a/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -15,7 +15,9 @@ class MapPageComponent extends React.Component { selected: null, selectedType: null, killPressed: false, - showKillDialog: false + showKillDialog: false, + telemetry: [], + telemetryLastTimestamp: null }; } @@ -25,13 +27,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()) @@ -44,6 +51,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]) @@ -104,6 +126,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 (
@@ -122,17 +164,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 c4bbecc62..12ce23d58 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -277,13 +277,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 { diff --git a/monkey_island/deb-package/DEBIAN/postinst b/monkey_island/deb-package/DEBIAN/postinst index 42b8f1eb0..3fa922a01 100644 --- a/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey_island/deb-package/DEBIAN/postinst @@ -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} 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 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..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 -python main.py \ No newline at end of file +/var/monkey_island/bin/python/bin/python main.py \ No newline at end of file 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/create_certificate.bat b/monkey_island/windows/create_certificate.bat index ac6555f0b..0af3e9960 100644 --- a/monkey_island/windows/create_certificate.bat +++ b/monkey_island/windows/create_certificate.bat @@ -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 \ No newline at end of file +@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" \ No newline at end of file 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) 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 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