diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1c84f4c45..00c0f3c86 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,8 @@ * [ ] Have you added an explanation of what your changes do and why you'd like to include them? * [ ] Have you successfully tested your changes locally? +* Example screenshot/log transcript of the feature working + ## Changes - - diff --git a/.github/Security-overview.png b/.github/Security-overview.png new file mode 100644 index 000000000..9f5fc7047 Binary files /dev/null and b/.github/Security-overview.png differ diff --git a/.github/map-full.png b/.github/map-full.png index 5416aa8fb..faa1b7832 100644 Binary files a/.github/map-full.png and b/.github/map-full.png differ diff --git a/README.md b/README.md index dfe501470..64a6d29ab 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,15 @@ Infection Monkey Welcome to the Infection Monkey! -The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infection. The Monkey uses various methods to self propagate across a data center and reports success to a centralized Command and Control(C&C) server. +The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infection. The Monkey uses various methods to self propagate across a data center and reports success to a centralized Monkey Island server. -![Infection Monkey map](.github/map-full.png) + + + The Infection Monkey is comprised of two parts: * Monkey - A tool which infects other machines and propagates to them -* Monkey Island - A C&C server with a dedicated UI to visualize the Chaos Monkey's progress inside the data center +* Monkey Island - A dedicated server to control and visualize the Infection Monkey's progress inside the data center To read more about the Monkey, visit http://infectionmonkey.com @@ -24,7 +26,7 @@ The Infection Monkey uses the following techniques and exploits to propagate to * Multiple propagation techniques: * Predefined passwords * Common logical exploits - * Password stealing using mimikatz + * Password stealing using Mimikatz * Multiple exploit methods: * SSH * SMB @@ -43,7 +45,7 @@ Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in t Building the Monkey from source ------------------------------- If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/setup) -and follow the instructions at the readme files under [chaos_monkey](chaos_monkey) and [monkey_island](monkey_island). +and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island). License diff --git a/chaos_monkey/network/tcp_scanner.py b/chaos_monkey/network/tcp_scanner.py deleted file mode 100644 index 8ce715f7f..000000000 --- a/chaos_monkey/network/tcp_scanner.py +++ /dev/null @@ -1,45 +0,0 @@ -import time -from random import shuffle -from network import HostScanner, HostFinger -from model.host import VictimHost -from network.tools import check_port_tcp - -__author__ = 'itamar' - -BANNER_READ = 1024 - - -class TcpScanner(HostScanner, HostFinger): - def __init__(self): - self._config = __import__('config').WormConfiguration - - def is_host_alive(self, host): - return self.get_host_fingerprint(host, True) - - def get_host_fingerprint(self, host, only_one_port=False): - assert isinstance(host, VictimHost) - - count = 0 - # maybe hide under really bad detection systems - target_ports = self._config.tcp_target_ports[:] - shuffle(target_ports) - - for target_port in target_ports: - - is_open, banner = check_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 diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py deleted file mode 100644 index 66f4eef57..000000000 --- a/chaos_monkey/network/tools.py +++ /dev/null @@ -1,79 +0,0 @@ -import socket -import select -import logging -import struct - -DEFAULT_TIMEOUT = 10 -BANNER_READ = 1024 - -LOG = logging.getLogger(__name__) - - -def struct_unpack_tracker(data, index, fmt): - """ - Unpacks a struct from the specified index according to specified format. - Returns the data and the next index - :param data: Buffer - :param index: Position index - :param fmt: Struct format - :return: (Data, new index) - """ - unpacked = struct.unpack_from(fmt, data, index) - return unpacked, struct.calcsize(fmt) - - -def struct_unpack_tracker_string(data, index): - """ - Unpacks a null terminated string from the specified index - Returns the data and the next index - :param data: Buffer - :param index: Position index - :return: (Data, new index) - """ - ascii_len = data[index:].find('\0') - fmt = "%ds" % ascii_len - return struct_unpack_tracker(data,index,fmt) - - -def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - - try: - sock.connect((ip, port)) - except socket.timeout: - return False, None - except socket.error, exc: - LOG.debug("Check port: %s:%s, Exception: %s", ip, port, exc) - return False, None - - banner = None - - try: - if get_banner: - read_ready, _, _ = select.select([sock], [], [], timeout) - if len(read_ready) > 0: - banner = sock.recv(BANNER_READ) - except: - pass - - sock.close() - return True, banner - - -def check_port_udp(ip, port, timeout=DEFAULT_TIMEOUT): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(timeout) - - data = None - is_open = False - - try: - sock.sendto("-", (ip, port)) - data, _ = sock.recvfrom(BANNER_READ) - is_open = True - except: - pass - sock.close() - - return is_open, data diff --git a/chaos_monkey/build_linux.sh b/infection_monkey/build_linux.sh similarity index 100% rename from chaos_monkey/build_linux.sh rename to infection_monkey/build_linux.sh diff --git a/chaos_monkey/build_windows.bat b/infection_monkey/build_windows.bat similarity index 100% rename from chaos_monkey/build_windows.bat rename to infection_monkey/build_windows.bat diff --git a/chaos_monkey/config.py b/infection_monkey/config.py similarity index 100% rename from chaos_monkey/config.py rename to infection_monkey/config.py diff --git a/chaos_monkey/control.py b/infection_monkey/control.py similarity index 98% rename from chaos_monkey/control.py rename to infection_monkey/control.py index b4f2769cd..e7fb4cebb 100644 --- a/chaos_monkey/control.py +++ b/infection_monkey/control.py @@ -25,7 +25,7 @@ class ControlClient(object): @staticmethod def wakeup(parent=None, default_tunnel=None, has_internet_access=None): - LOG.debug("Trying to wake up with C&C servers list: %r" % WormConfiguration.command_servers) + LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers) if parent or default_tunnel: LOG.debug("parent: %s, default_tunnel: %s" % (parent, default_tunnel)) hostname = gethostname() diff --git a/chaos_monkey/dropper.py b/infection_monkey/dropper.py similarity index 100% rename from chaos_monkey/dropper.py rename to infection_monkey/dropper.py diff --git a/chaos_monkey/example.conf b/infection_monkey/example.conf similarity index 100% rename from chaos_monkey/example.conf rename to infection_monkey/example.conf diff --git a/chaos_monkey/exploit/__init__.py b/infection_monkey/exploit/__init__.py similarity index 100% rename from chaos_monkey/exploit/__init__.py rename to infection_monkey/exploit/__init__.py diff --git a/chaos_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py similarity index 100% rename from chaos_monkey/exploit/elasticgroovy.py rename to infection_monkey/exploit/elasticgroovy.py diff --git a/chaos_monkey/exploit/rdpgrinder.py b/infection_monkey/exploit/rdpgrinder.py similarity index 98% rename from chaos_monkey/exploit/rdpgrinder.py rename to infection_monkey/exploit/rdpgrinder.py index 207564778..606f44f90 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/infection_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/sambacry.py b/infection_monkey/exploit/sambacry.py similarity index 100% rename from chaos_monkey/exploit/sambacry.py rename to infection_monkey/exploit/sambacry.py diff --git a/chaos_monkey/exploit/shellshock.py b/infection_monkey/exploit/shellshock.py similarity index 100% rename from chaos_monkey/exploit/shellshock.py rename to infection_monkey/exploit/shellshock.py diff --git a/chaos_monkey/exploit/shellshock_resources.py b/infection_monkey/exploit/shellshock_resources.py similarity index 100% rename from chaos_monkey/exploit/shellshock_resources.py rename to infection_monkey/exploit/shellshock_resources.py diff --git a/chaos_monkey/exploit/smbexec.py b/infection_monkey/exploit/smbexec.py similarity index 97% rename from chaos_monkey/exploit/smbexec.py rename to infection_monkey/exploit/smbexec.py index f5fa2b26b..b76a7bce6 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/infection_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/infection_monkey/exploit/sshexec.py similarity index 98% rename from chaos_monkey/exploit/sshexec.py rename to infection_monkey/exploit/sshexec.py index f58e5677b..b93970ca9 100644 --- a/chaos_monkey/exploit/sshexec.py +++ b/infection_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/tools.py b/infection_monkey/exploit/tools.py similarity index 100% rename from chaos_monkey/exploit/tools.py rename to infection_monkey/exploit/tools.py diff --git a/chaos_monkey/exploit/win_ms08_067.py b/infection_monkey/exploit/win_ms08_067.py similarity index 99% rename from chaos_monkey/exploit/win_ms08_067.py rename to infection_monkey/exploit/win_ms08_067.py index 3ed553931..51393ea69 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/infection_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/exploit/wmiexec.py b/infection_monkey/exploit/wmiexec.py similarity index 100% rename from chaos_monkey/exploit/wmiexec.py rename to infection_monkey/exploit/wmiexec.py diff --git a/chaos_monkey/main.py b/infection_monkey/main.py similarity index 98% rename from chaos_monkey/main.py rename to infection_monkey/main.py index c53232b2c..ea8ee769a 100644 --- a/chaos_monkey/main.py +++ b/infection_monkey/main.py @@ -11,7 +11,7 @@ import traceback from config import WormConfiguration, EXTERNAL_CONFIG_FILE from dropper import MonkeyDrops from model import MONKEY_ARG, DROPPER_ARG -from monkey import ChaosMonkey +from monkey import InfectionMonkey if __name__ == "__main__": sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -80,7 +80,7 @@ def main(): if MONKEY_ARG == monkey_mode: log_path = os.path.expandvars( WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" else WormConfiguration.monkey_log_path_linux - monkey_cls = ChaosMonkey + monkey_cls = InfectionMonkey elif DROPPER_ARG == monkey_mode: log_path = os.path.expandvars( WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" else WormConfiguration.dropper_log_path_linux diff --git a/chaos_monkey/model/__init__.py b/infection_monkey/model/__init__.py similarity index 100% rename from chaos_monkey/model/__init__.py rename to infection_monkey/model/__init__.py diff --git a/chaos_monkey/model/host.py b/infection_monkey/model/host.py similarity index 100% rename from chaos_monkey/model/host.py rename to infection_monkey/model/host.py diff --git a/chaos_monkey/monkey-linux.spec b/infection_monkey/monkey-linux.spec similarity index 100% rename from chaos_monkey/monkey-linux.spec rename to infection_monkey/monkey-linux.spec diff --git a/chaos_monkey/monkey.ico b/infection_monkey/monkey.ico similarity index 100% rename from chaos_monkey/monkey.ico rename to infection_monkey/monkey.ico diff --git a/chaos_monkey/monkey.py b/infection_monkey/monkey.py similarity index 99% rename from chaos_monkey/monkey.py rename to infection_monkey/monkey.py index 79012dc39..22be2cf46 100644 --- a/chaos_monkey/monkey.py +++ b/infection_monkey/monkey.py @@ -19,7 +19,7 @@ __author__ = 'itamar' LOG = logging.getLogger(__name__) -class ChaosMonkey(object): +class InfectionMonkey(object): def __init__(self, args): self._keep_running = False self._exploited_machines = set() diff --git a/chaos_monkey/monkey.spec b/infection_monkey/monkey.spec similarity index 100% rename from chaos_monkey/monkey.spec rename to infection_monkey/monkey.spec diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh similarity index 100% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh rename to infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c similarity index 100% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c rename to infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h rename to infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h diff --git a/chaos_monkey/monkeyfs.py b/infection_monkey/monkeyfs.py similarity index 100% rename from chaos_monkey/monkeyfs.py rename to infection_monkey/monkeyfs.py diff --git a/chaos_monkey/network/__init__.py b/infection_monkey/network/__init__.py similarity index 100% rename from chaos_monkey/network/__init__.py rename to infection_monkey/network/__init__.py diff --git a/chaos_monkey/network/elasticfinger.py b/infection_monkey/network/elasticfinger.py similarity index 100% rename from chaos_monkey/network/elasticfinger.py rename to infection_monkey/network/elasticfinger.py diff --git a/chaos_monkey/network/firewall.py b/infection_monkey/network/firewall.py similarity index 100% rename from chaos_monkey/network/firewall.py rename to infection_monkey/network/firewall.py diff --git a/chaos_monkey/network/httpfinger.py b/infection_monkey/network/httpfinger.py similarity index 100% rename from chaos_monkey/network/httpfinger.py rename to infection_monkey/network/httpfinger.py diff --git a/chaos_monkey/network/info.py b/infection_monkey/network/info.py similarity index 100% rename from chaos_monkey/network/info.py rename to infection_monkey/network/info.py diff --git a/chaos_monkey/network/mysqlfinger.py b/infection_monkey/network/mysqlfinger.py similarity index 100% rename from chaos_monkey/network/mysqlfinger.py rename to infection_monkey/network/mysqlfinger.py diff --git a/chaos_monkey/network/network_scanner.py b/infection_monkey/network/network_scanner.py similarity index 88% rename from chaos_monkey/network/network_scanner.py rename to infection_monkey/network/network_scanner.py index 5a9037184..9c1cf897e 100644 --- a/chaos_monkey/network/network_scanner.py +++ b/infection_monkey/network/network_scanner.py @@ -1,9 +1,10 @@ -import time import logging -from . import HostScanner +import time + from config import WormConfiguration from info import local_ips, get_ips_from_interfaces from range import * +from . import HostScanner __author__ = 'itamar' @@ -18,6 +19,12 @@ class NetworkScanner(object): self._ranges = None def initialize(self): + """ + Set up scanning based on configuration + FixedRange -> Reads from range_fixed field in configuration + otherwise, takes a range from every IP address the current host has. + :return: + """ # get local ip addresses self._ip_addresses = local_ips() @@ -27,7 +34,7 @@ class NetworkScanner(object): LOG.info("Found local IP addresses of the machine: %r", self._ip_addresses) # for fixed range, only scan once. if WormConfiguration.range_class is FixedRange: - self._ranges = [WormConfiguration.range_class(None)] + self._ranges = [WormConfiguration.range_class(fixed_addresses=WormConfiguration.range_fixed)] else: self._ranges = [WormConfiguration.range_class(ip_address) for ip_address in self._ip_addresses] diff --git a/chaos_monkey/network/ping_scanner.py b/infection_monkey/network/ping_scanner.py similarity index 98% rename from chaos_monkey/network/ping_scanner.py rename to infection_monkey/network/ping_scanner.py index 842a6aee8..7162c36f3 100644 --- a/chaos_monkey/network/ping_scanner.py +++ b/infection_monkey/network/ping_scanner.py @@ -1,10 +1,11 @@ -import os -import sys -import subprocess import logging -from . import HostScanner, HostFinger -from model.host import VictimHost +import os import re +import subprocess +import sys + +from model.host import VictimHost +from . import HostScanner, HostFinger __author__ = 'itamar' @@ -62,7 +63,7 @@ class PingScanner(HostScanner, HostFinger): elif WINDOWS_TTL == ttl: host.os['type'] = 'windows' return True - except Exception, exc: + except Exception as exc: LOG.debug("Error parsing ping fingerprint: %s", exc) return False diff --git a/chaos_monkey/network/range.py b/infection_monkey/network/range.py similarity index 98% rename from chaos_monkey/network/range.py rename to infection_monkey/network/range.py index fdd29bc09..b07828f4b 100644 --- a/chaos_monkey/network/range.py +++ b/infection_monkey/network/range.py @@ -1,7 +1,8 @@ -import socket import random +import socket import struct from abc import ABCMeta, abstractmethod + from model.host import VictimHost __author__ = 'itamar' @@ -77,5 +78,5 @@ class FixedRange(NetworkRange): for address in self._fixed_addresses: if not address: # Empty string continue - address_range.append(struct.unpack(">L", socket.inet_aton(address))[0]) + address_range.append(struct.unpack(">L", socket.inet_aton(address.strip()))[0]) return address_range diff --git a/chaos_monkey/network/smbfinger.py b/infection_monkey/network/smbfinger.py similarity index 100% rename from chaos_monkey/network/smbfinger.py rename to infection_monkey/network/smbfinger.py diff --git a/chaos_monkey/network/sshfinger.py b/infection_monkey/network/sshfinger.py similarity index 93% rename from chaos_monkey/network/sshfinger.py rename to infection_monkey/network/sshfinger.py index 75a3380ca..89c3092d7 100644 --- a/chaos_monkey/network/sshfinger.py +++ b/infection_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/infection_monkey/network/tcp_scanner.py b/infection_monkey/network/tcp_scanner.py new file mode 100644 index 000000000..e291e8d3e --- /dev/null +++ b/infection_monkey/network/tcp_scanner.py @@ -0,0 +1,41 @@ +from itertools import izip_longest +from random import shuffle + +from network import HostScanner, HostFinger +from network.tools import check_tcp_ports + +__author__ = 'itamar' + +BANNER_READ = 1024 + + +class TcpScanner(HostScanner, HostFinger): + def __init__(self): + self._config = __import__('config').WormConfiguration + + def is_host_alive(self, host): + return self.get_host_fingerprint(host, True) + + def get_host_fingerprint(self, host, only_one_port=False): + """ + 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. + """ + + # maybe hide under really bad detection systems + target_ports = self._config.tcp_target_ports[:] + shuffle(target_ports) + + ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0, + self._config.tcp_scan_get_banner) + for target_port, banner in izip_longest(ports, banners, fillvalue=None): + service = 'tcp-' + str(target_port) + host.services[service] = {} + if banner: + host.services[service]['banner'] = banner + if only_one_port: + break + + return len(ports) != 0 diff --git a/infection_monkey/network/tools.py b/infection_monkey/network/tools.py new file mode 100644 index 000000000..eac020dc0 --- /dev/null +++ b/infection_monkey/network/tools.py @@ -0,0 +1,150 @@ +import logging +import select +import socket +import struct +import time + +DEFAULT_TIMEOUT = 10 +BANNER_READ = 1024 + +LOG = logging.getLogger(__name__) + + +def struct_unpack_tracker(data, index, fmt): + """ + Unpacks a struct from the specified index according to specified format. + Returns the data and the next index + :param data: Buffer + :param index: Position index + :param fmt: Struct format + :return: (Data, new index) + """ + unpacked = struct.unpack_from(fmt, data, index) + return unpacked, struct.calcsize(fmt) + + +def struct_unpack_tracker_string(data, index): + """ + Unpacks a null terminated string from the specified index + Returns the data and the next index + :param data: Buffer + :param index: Position index + :return: (Data, new index) + """ + ascii_len = data[index:].find('\0') + fmt = "%ds" % ascii_len + return struct_unpack_tracker(data, index, fmt) + + +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) + + try: + sock.connect((ip, port)) + except socket.timeout: + return False, None + except socket.error as exc: + LOG.debug("Check port: %s:%s, Exception: %s", ip, port, exc) + return False, None + + banner = None + + try: + if get_banner: + read_ready, _, _ = select.select([sock], [], [], timeout) + if len(read_ready) > 0: + banner = sock.recv(BANNER_READ) + except socket.error: + pass + + sock.close() + return True, banner + + +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 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] + good_ports = [] + try: + LOG.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports))) + for sock, port in zip(sockets, ports): + err = sock.connect_ex((ip, port)) + if err == 0: + good_ports.append((port, sock)) + continue + if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + good_ports.append((port, sock)) + continue + if err == 115: # EINPROGRESS 115 /* Operation now in progress */ + good_ports.append((port, sock)) + continue + LOG.warning("Failed to connect to port %s, error code is %d", port, err) + + if len(good_ports) != 0: + time.sleep(timeout) + # this is possibly connected. meaning after timeout wait, we expect to see a connection up + # Possible valid errors codes if we chose to check for actually closed are + # ECONNREFUSED (111) or WSAECONNREFUSED (10061) or WSAETIMEDOUT(10060) + connected_ports_sockets = [s for s in good_ports if + s[1].getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) == 0] + LOG.debug( + "On host %s discovered the following ports %s" % + (str(ip), ",".join([str(x[0]) for x in connected_ports_sockets]))) + banners = [] + if get_banner: + readable_sockets, _, _ = select.select([s[1] for s in connected_ports_sockets], [], [], 0) + # read first BANNER_READ bytes + banners = [sock.recv(BANNER_READ) if sock in readable_sockets else "" + for port, sock in connected_ports_sockets] + pass + # try to cleanup + [s[1].close() for s in good_ports] + return [port for port, sock in connected_ports_sockets], banners + else: + return [], [] + + except socket.error as exc: + LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) + return [], [] diff --git a/chaos_monkey/readme.txt b/infection_monkey/readme.txt similarity index 86% rename from chaos_monkey/readme.txt rename to infection_monkey/readme.txt index bdf267f60..67c4033d9 100644 --- a/chaos_monkey/readme.txt +++ b/infection_monkey/readme.txt @@ -28,13 +28,13 @@ The monkey is composed of three separate parts. 64bit: http://www.microsoft.com/en-us/download/details.aspx?id=13523 6. Download the dependent python packages using pip install -r requirements.txt -7. Download and extract UPX binary to [source-path]\monkey\chaos_monkey\bin\upx.exe: +7. Download and extract UPX binary to [source-path]\monkey\infection_monkey\bin\upx.exe: https://github.com/upx/upx/releases/download/v3.94/upx394w.zip 8. Build/Download Sambacry and Mimikatz binaries a. Build/Download according to sections at the end of this readme. - b. Place the binaries under [code location]\chaos_monkey\bin + b. Place the binaries under [code location]\infection_monkey\bin 9. To build the final exe: - cd [code location]/chaos_monkey + cd [code location]/infection_monkey build_windows.bat output is placed under dist\monkey.exe @@ -46,13 +46,13 @@ Tested on Ubuntu 16.04 and 17.04. sudo apt-get update sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1 Install the python packages listed in requirements.txt using pip - cd [code location]/chaos_monkey + cd [code location]/infection_monkey pip install -r requirements.txt 2. Build Sambacry binaries a. Build/Download according to sections at the end of this readme. - b. Place the binaries under [code location]\chaos_monkey\bin + b. Place the binaries under [code location]\infection_monkey\bin 3. To build, run in terminal: - cd [code location]/chaos_monkey + cd [code location]/infection_monkey chmod +x build_linux.sh ./build_linux.sh output is placed under dist/monkey @@ -63,11 +63,11 @@ Sambacry requires two standalone binaries to execute remotely. 1. Install gcc-multilib if it's not installed sudo apt-get install gcc-multilib 2. Build the binaries - cd [code location]/chaos_monkey/monkey_utils/sambacry_monkey_runner + cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner ./build.sh -- Mimikatz -- Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile from sources (requires Visual Studio 2013 and up) or download the binaries from https://github.com/guardicore/mimikatz/releases/tag/1.0.0 -Download both 32 and 64 bit DLLs and place them under [code location]\chaos_monkey\bin \ No newline at end of file +Download both 32 and 64 bit DLLs and place them under [code location]\infection_monkey\bin \ No newline at end of file diff --git a/chaos_monkey/requirements.txt b/infection_monkey/requirements.txt similarity index 100% rename from chaos_monkey/requirements.txt rename to infection_monkey/requirements.txt diff --git a/chaos_monkey/system_info/__init__.py b/infection_monkey/system_info/__init__.py similarity index 100% rename from chaos_monkey/system_info/__init__.py rename to infection_monkey/system_info/__init__.py diff --git a/chaos_monkey/system_info/linux_info_collector.py b/infection_monkey/system_info/linux_info_collector.py similarity index 100% rename from chaos_monkey/system_info/linux_info_collector.py rename to infection_monkey/system_info/linux_info_collector.py diff --git a/chaos_monkey/system_info/mimikatz_collector.py b/infection_monkey/system_info/mimikatz_collector.py similarity index 100% rename from chaos_monkey/system_info/mimikatz_collector.py rename to infection_monkey/system_info/mimikatz_collector.py diff --git a/chaos_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py similarity index 100% rename from chaos_monkey/system_info/windows_info_collector.py rename to infection_monkey/system_info/windows_info_collector.py diff --git a/chaos_monkey/system_singleton.py b/infection_monkey/system_singleton.py similarity index 100% rename from chaos_monkey/system_singleton.py rename to infection_monkey/system_singleton.py diff --git a/chaos_monkey/test/__init__.py b/infection_monkey/test/__init__.py similarity index 100% rename from chaos_monkey/test/__init__.py rename to infection_monkey/test/__init__.py diff --git a/chaos_monkey/test/config__test.py b/infection_monkey/test/config__test.py similarity index 94% rename from chaos_monkey/test/config__test.py rename to infection_monkey/test/config__test.py index fccde2f0d..accdd5a49 100644 --- a/chaos_monkey/test/config__test.py +++ b/infection_monkey/test/config__test.py @@ -1,5 +1,5 @@ # -*- coding: UTF-8 -*- -# NOTE: Launch all tests with `nosetests` command from chaos_monkey dir. +# NOTE: Launch all tests with `nosetests` command from infection_monkey dir. import json import unittest diff --git a/chaos_monkey/transport/__init__.py b/infection_monkey/transport/__init__.py similarity index 100% rename from chaos_monkey/transport/__init__.py rename to infection_monkey/transport/__init__.py diff --git a/chaos_monkey/transport/base.py b/infection_monkey/transport/base.py similarity index 100% rename from chaos_monkey/transport/base.py rename to infection_monkey/transport/base.py diff --git a/chaos_monkey/transport/ftp.py b/infection_monkey/transport/ftp.py similarity index 100% rename from chaos_monkey/transport/ftp.py rename to infection_monkey/transport/ftp.py diff --git a/chaos_monkey/transport/http.py b/infection_monkey/transport/http.py similarity index 100% rename from chaos_monkey/transport/http.py rename to infection_monkey/transport/http.py diff --git a/chaos_monkey/transport/tcp.py b/infection_monkey/transport/tcp.py similarity index 100% rename from chaos_monkey/transport/tcp.py rename to infection_monkey/transport/tcp.py diff --git a/chaos_monkey/tunnel.py b/infection_monkey/tunnel.py similarity index 98% rename from chaos_monkey/tunnel.py rename to infection_monkey/tunnel.py index 7f7edec03..9a50679ff 100644 --- a/chaos_monkey/tunnel.py +++ b/infection_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 9c85f6230..4733d5089 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -1,22 +1,26 @@ +import os from datetime import datetime + import bson -from bson.json_util import dumps -from flask import Flask, send_from_directory, redirect, make_response import flask_restful +from bson.json_util import dumps +from flask import Flask, send_from_directory, make_response from werkzeug.exceptions import NotFound +from cc.auth import init_jwt from cc.database import mongo +from cc.environment.environment import env from cc.resources.client_run import ClientRun -from cc.resources.monkey import Monkey +from cc.resources.edge import Edge from cc.resources.local_run import LocalRun -from cc.resources.telemetry import Telemetry +from cc.resources.monkey import Monkey from cc.resources.monkey_configuration import MonkeyConfiguration 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 import Telemetry from cc.resources.telemetry_feed import TelemetryFeed from cc.services.config import ConfigService @@ -70,6 +74,12 @@ def init_app(mongo_url): api.representations = {'application/json': output_json} app.config['MONGO_URI'] = mongo_url + + app.config['SECRET_KEY'] = os.urandom(32) + app.config['JWT_AUTH_URL_RULE'] = '/api/auth' + app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time() + + init_jwt(app) mongo.init_app(app) with app.app_context(): diff --git a/monkey_island/cc/auth.py b/monkey_island/cc/auth.py new file mode 100644 index 000000000..a32d6ec9d --- /dev/null +++ b/monkey_island/cc/auth.py @@ -0,0 +1,53 @@ +from functools import wraps + +from flask import current_app, abort +from flask_jwt import JWT, _jwt_required, JWTError +from werkzeug.security import safe_str_cmp + +from cc.environment.environment import env + +__author__ = 'itay.mizeretz' + + +class User(object): + def __init__(self, id, username, secret): + self.id = id + self.username = username + self.secret = secret + + def __str__(self): + return "User(id='%s')" % self.id + + +def init_jwt(app): + users = env.get_auth_users() + username_table = {u.username: u for u in users} + userid_table = {u.id: u for u in users} + + def authenticate(username, secret): + user = username_table.get(username, None) + if user and safe_str_cmp(user.secret.encode('utf-8'), secret.encode('utf-8')): + return user + + def identity(payload): + user_id = payload['identity'] + return userid_table.get(user_id, None) + + if env.is_auth_enabled(): + JWT(app, authenticate, identity) + + +def jwt_required(realm=None): + def wrapper(fn): + @wraps(fn) + def decorator(*args, **kwargs): + if env.is_auth_enabled(): + try: + _jwt_required(realm or current_app.config['JWT_DEFAULT_REALM']) + except JWTError: + abort(401) + return fn(*args, **kwargs) + + return decorator + + return wrapper diff --git a/monkey_island/cc/environment/__init__.py b/monkey_island/cc/environment/__init__.py new file mode 100644 index 000000000..712ba232a --- /dev/null +++ b/monkey_island/cc/environment/__init__.py @@ -0,0 +1,33 @@ +import abc +from datetime import timedelta + +__author__ = 'itay.mizeretz' + + +class Environment(object): + __metaclass__ = abc.ABCMeta + + _ISLAND_PORT = 5000 + _MONGO_URL = "mongodb://localhost:27017/monkeyisland" + _DEBUG_SERVER = False + _AUTH_EXPIRATION_TIME = timedelta(hours=1) + + def get_island_port(self): + return self._ISLAND_PORT + + def get_mongo_url(self): + return self._MONGO_URL + + def is_debug(self): + return self._DEBUG_SERVER + + def get_auth_expiration_time(self): + return self._AUTH_EXPIRATION_TIME + + @abc.abstractmethod + def is_auth_enabled(self): + return + + @abc.abstractmethod + def get_auth_users(self): + return diff --git a/monkey_island/cc/environment/aws.py b/monkey_island/cc/environment/aws.py new file mode 100644 index 000000000..b85a7d2e4 --- /dev/null +++ b/monkey_island/cc/environment/aws.py @@ -0,0 +1,24 @@ +import urllib2 + +import cc.auth +from cc.environment import Environment + +__author__ = 'itay.mizeretz' + + +class AwsEnvironment(Environment): + def __init__(self): + super(AwsEnvironment, self).__init__() + self._instance_id = AwsEnvironment._get_instance_id() + + @staticmethod + def _get_instance_id(): + return urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() + + def is_auth_enabled(self): + return True + + def get_auth_users(self): + return [ + cc.auth.User(1, 'monkey', self._instance_id) + ] diff --git a/monkey_island/cc/environment/environment.py b/monkey_island/cc/environment/environment.py new file mode 100644 index 000000000..8eb97a999 --- /dev/null +++ b/monkey_island/cc/environment/environment.py @@ -0,0 +1,23 @@ +import json +import standard +import aws + +ENV_DICT = { + 'standard': standard.StandardEnvironment, + 'aws': aws.AwsEnvironment +} + + +def load_env_from_file(): + with open('server_config.json', 'r') as f: + config_content = f.read() + config_json = json.loads(config_content) + return config_json['server_config'] + + +try: + __env_type = load_env_from_file() + env = ENV_DICT[__env_type]() +except Exception: + print('Failed initializing environment: %s' % __env_type) + raise diff --git a/monkey_island/cc/environment/standard.py b/monkey_island/cc/environment/standard.py new file mode 100644 index 000000000..8df00a2c3 --- /dev/null +++ b/monkey_island/cc/environment/standard.py @@ -0,0 +1,12 @@ +from cc.environment import Environment + +__author__ = 'itay.mizeretz' + + +class StandardEnvironment(Environment): + + def is_auth_enabled(self): + return False + + def get_auth_users(self): + return [] diff --git a/monkey_island/cc/island_config.py b/monkey_island/cc/island_config.py deleted file mode 100644 index 0a8f33bac..000000000 --- a/monkey_island/cc/island_config.py +++ /dev/null @@ -1,5 +0,0 @@ -__author__ = 'itay.mizeretz' - -ISLAND_PORT = 5000 -DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" -DEBUG_SERVER = False diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index bb1ed9eaf..e0f6ab079 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -11,7 +11,7 @@ if BASE_PATH not in sys.path: from cc.app import init_app from cc.utils import local_ip_addresses -from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT, DEBUG_SERVER +from cc.environment.environment import env from cc.database import is_db_server_up if __name__ == '__main__': @@ -19,20 +19,20 @@ if __name__ == '__main__': from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop - mongo_url = os.environ.get('MONGO_URL', DEFAULT_MONGO_URL) + mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) while not is_db_server_up(mongo_url): print('Waiting for MongoDB server') time.sleep(1) app = init_app(mongo_url) - if DEBUG_SERVER: + if env.is_debug(): 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)) + http_server.listen(env.get_island_port()) + print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port())) IOLoop.instance().start() diff --git a/monkey_island/cc/resources/local_run.py b/monkey_island/cc/resources/local_run.py index 3d18b49e6..c588eaf80 100644 --- a/monkey_island/cc/resources/local_run.py +++ b/monkey_island/cc/resources/local_run.py @@ -6,8 +6,8 @@ import sys from flask import request, jsonify, make_response import flask_restful +from cc.environment.environment import env from cc.resources.monkey_download import get_monkey_executable -from cc.island_config import ISLAND_PORT from cc.services.node import NodeService from cc.utils import local_ip_addresses @@ -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], env.get_island_port())] if sys.platform == "win32": args = "".join(args) pid = subprocess.Popen(args, shell=True).pid diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 37722262c..d344949bc 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -15,23 +15,20 @@ __author__ = 'Barak' class Monkey(flask_restful.Resource): + + # Used by monkey. can't secure. def get(self, guid=None, **kw): NodeService.update_dead_monkeys() # refresh monkeys status if not guid: guid = request.args.get('guid') - timestamp = request.args.get('timestamp') if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) return monkey_json - else: - result = {'timestamp': datetime.now().isoformat()} - find_filter = {} - if timestamp is not None: - find_filter['modifytime'] = {'$gt': dateutil.parser.parse(timestamp)} - result['objects'] = [x for x in mongo.db.monkey.find(find_filter)] - return result + return {} + + # Used by monkey. can't secure. def patch(self, guid): monkey_json = json.loads(request.data) update = {"$set": {'modifytime': datetime.now()}} @@ -51,6 +48,7 @@ class Monkey(flask_restful.Resource): return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) + # Used by monkey. can't secure. def post(self, **kw): monkey_json = json.loads(request.data) monkey_json['creds'] = [] diff --git a/monkey_island/cc/resources/monkey_configuration.py b/monkey_island/cc/resources/monkey_configuration.py index c3c9f51cb..81ceab56e 100644 --- a/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey_island/cc/resources/monkey_configuration.py @@ -1,18 +1,20 @@ import json -from flask import request, jsonify import flask_restful +from flask import request, jsonify -from cc.database import mongo +from cc.auth import jwt_required from cc.services.config import ConfigService __author__ = 'Barak' class MonkeyConfiguration(flask_restful.Resource): + @jwt_required() def get(self): return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config()) + @jwt_required() def post(self): config_json = json.loads(request.data) if config_json.has_key('reset'): @@ -20,4 +22,3 @@ class MonkeyConfiguration(flask_restful.Resource): else: ConfigService.update_config(config_json, should_encrypt=True) return self.get() - diff --git a/monkey_island/cc/resources/monkey_download.py b/monkey_island/cc/resources/monkey_download.py index b311c4472..ac1f9de2d 100644 --- a/monkey_island/cc/resources/monkey_download.py +++ b/monkey_island/cc/resources/monkey_download.py @@ -47,9 +47,12 @@ def get_monkey_executable(host_os, machine): class MonkeyDownload(flask_restful.Resource): + + # Used by monkey. can't secure. def get(self, path): return send_from_directory('binaries', path) + # Used by monkey. can't secure. def post(self): host_json = json.loads(request.data) host_os = host_json.get('os') diff --git a/monkey_island/cc/resources/netmap.py b/monkey_island/cc/resources/netmap.py index 12418ef6b..3ba7fafa8 100644 --- a/monkey_island/cc/resources/netmap.py +++ b/monkey_island/cc/resources/netmap.py @@ -1,5 +1,6 @@ import flask_restful +from cc.auth import jwt_required from cc.services.edge import EdgeService from cc.services.node import NodeService from cc.database import mongo @@ -8,6 +9,7 @@ __author__ = 'Barak' class NetMap(flask_restful.Resource): + @jwt_required() def get(self, **kw): monkeys = [NodeService.monkey_to_net_node(x) for x in mongo.db.monkey.find({})] nodes = [NodeService.node_to_net_node(x) for x in mongo.db.node.find({})] diff --git a/monkey_island/cc/resources/node.py b/monkey_island/cc/resources/node.py index 5a6c52e1b..bc00c40cf 100644 --- a/monkey_island/cc/resources/node.py +++ b/monkey_island/cc/resources/node.py @@ -1,12 +1,14 @@ from flask import request import flask_restful +from cc.auth import jwt_required from cc.services.node import NodeService __author__ = 'Barak' class Node(flask_restful.Resource): + @jwt_required() def get(self): node_id = request.args.get('id') if node_id: diff --git a/monkey_island/cc/resources/report.py b/monkey_island/cc/resources/report.py index e967b207f..1a00fa609 100644 --- a/monkey_island/cc/resources/report.py +++ b/monkey_island/cc/resources/report.py @@ -1,10 +1,13 @@ import flask_restful +from cc.auth import jwt_required from cc.services.report import ReportService __author__ = "itay.mizeretz" class Report(flask_restful.Resource): + + @jwt_required() def get(self): return ReportService.get_report() diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index 25d7dfed7..04129f257 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -3,6 +3,7 @@ from datetime import datetime import flask_restful from flask import request, make_response, jsonify +from cc.auth import jwt_required from cc.database import mongo from cc.services.config import ConfigService from cc.services.node import NodeService @@ -13,6 +14,8 @@ __author__ = 'Barak' class Root(flask_restful.Resource): + + @jwt_required() def get(self, action=None): if not action: action = request.args.get('action') diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 6f69ecc2f..768f976b9 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -7,6 +7,7 @@ import dateutil import flask_restful from flask import request +from cc.auth import jwt_required from cc.database import mongo from cc.services.config import ConfigService from cc.services.edge import EdgeService @@ -17,6 +18,7 @@ __author__ = 'Barak' class Telemetry(flask_restful.Resource): + @jwt_required() def get(self, **kw): monkey_guid = request.args.get('monkey_guid') telem_type = request.args.get('telem_type') @@ -37,6 +39,7 @@ class Telemetry(flask_restful.Resource): result['objects'] = self.telemetry_to_displayed_telemetry(mongo.db.telemetry.find(find_filter)) return result + # Used by monkey. can't secure. def post(self): telemetry_json = json.loads(request.data) telemetry_json['timestamp'] = datetime.now() diff --git a/monkey_island/cc/resources/telemetry_feed.py b/monkey_island/cc/resources/telemetry_feed.py index 9a7e507ef..f14c5d29f 100644 --- a/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey_island/cc/resources/telemetry_feed.py @@ -5,6 +5,7 @@ import flask_restful from flask import request import flask_pymongo +from cc.auth import jwt_required from cc.database import mongo from cc.services.node import NodeService @@ -12,6 +13,7 @@ __author__ = 'itay.mizeretz' class TelemetryFeed(flask_restful.Resource): + @jwt_required() def get(self, **kw): timestamp = request.args.get('timestamp') if "null" == timestamp or timestamp is None: # special case to avoid ugly JS code... diff --git a/monkey_island/cc/server_config.json b/monkey_island/cc/server_config.json new file mode 100644 index 000000000..2d1a5995b --- /dev/null +++ b/monkey_island/cc/server_config.json @@ -0,0 +1,3 @@ +{ + "server_config": "standard" +} \ No newline at end of file diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 878520217..e2072b7cf 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -4,7 +4,7 @@ from jsonschema import Draft4Validator, validators from cc.database import mongo from cc.encryptor import encryptor -from cc.island_config import ISLAND_PORT +from cc.environment.environment import env from cc.utils import local_ip_addresses __author__ = "itay.mizeretz" @@ -531,7 +531,7 @@ SCHEMA = { } }, "cnc": { - "title": "C&C", + "title": "Monkey Island", "type": "object", "properties": { "servers": { @@ -912,8 +912,8 @@ class ConfigService: @staticmethod def set_server_ips_in_config(config): ips = local_ip_addresses() - 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) + config["cnc"]["servers"]["command_servers"] = ["%s:%d" % (ip, env.get_island_port()) for ip in ips] + config["cnc"]["servers"]["current_server"] = "%s:%d" % (ips[0], env.get_island_port()) @staticmethod def save_initial_config_if_needed(): diff --git a/monkey_island/cc/ui/package.json b/monkey_island/cc/ui/package.json index 5ee2e5389..6759c4530 100644 --- a/monkey_island/cc/ui/package.json +++ b/monkey_island/cc/ui/package.json @@ -65,6 +65,8 @@ "core-js": "^2.5.1", "fetch": "^1.1.0", "js-file-download": "^0.4.1", + "json-loader": "^0.5.7", + "jwt-decode": "^2.2.0", "normalize.css": "^4.0.0", "prop-types": "^15.5.10", "rc-progress": "^2.2.5", diff --git a/monkey_island/cc/ui/src/components/AuthComponent.js b/monkey_island/cc/ui/src/components/AuthComponent.js new file mode 100644 index 000000000..428c3272a --- /dev/null +++ b/monkey_island/cc/ui/src/components/AuthComponent.js @@ -0,0 +1,12 @@ +import React from 'react'; +import AuthService from '../services/AuthService'; + +class AuthComponent extends React.Component { + constructor(props) { + super(props); + this.auth = new AuthService(); + this.authFetch = this.auth.authFetch; + } +} + +export default AuthComponent; diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index 881c3a2ec..771e2257a 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -1,5 +1,5 @@ import React from 'react'; -import {NavLink, Route, BrowserRouter as Router} from 'react-router-dom'; +import {BrowserRouter as Router, NavLink, Redirect, Route} from 'react-router-dom'; import {Col, Grid, Row} from 'react-bootstrap'; import {Icon} from 'react-fa'; @@ -11,6 +11,8 @@ import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; import ReportPage from 'components/pages/ReportPage'; import LicensePage from 'components/pages/LicensePage'; +import AuthComponent from 'components/AuthComponent'; +import LoginPageComponent from 'components/pages/LoginPage'; require('normalize.css/normalize.css'); require('react-data-components/css/table-twbs.css'); @@ -22,7 +24,43 @@ 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 { +class AppComponent extends AuthComponent { + updateStatus = () => { + if (this.auth.loggedIn()){ + this.authFetch('/api') + .then(res => res.json()) + .then(res => { + // This check is used to prevent unnecessary re-rendering + let isChanged = false; + for (let step in this.state.completedSteps) { + if (this.state.completedSteps[step] !== res['completed_steps'][step]) { + isChanged = true; + break; + } + } + if (isChanged) { + this.setState({completedSteps: res['completed_steps']}); + } + }); + } + }; + + renderRoute = (route_path, page_component, is_exact_path = false) => { + let render_func = (props) => { + if (this.auth.loggedIn()) { + return page_component; + } else { + return ; + } + }; + + if (is_exact_path) { + return ; + } else { + return ; + } + }; + constructor(props) { super(props); this.state = { @@ -35,24 +73,6 @@ class AppComponent extends React.Component { }; } - updateStatus = () => { - fetch('/api') - .then(res => res.json()) - .then(res => { - // This check is used to prevent unnecessary re-rendering - let isChanged = false; - for (let step in this.state.completedSteps) { - if (this.state.completedSteps[step] !== res['completed_steps'][step]) { - isChanged = true; - break; - } - } - if (isChanged) { - this.setState({completedSteps: res['completed_steps']}); - } - }); - }; - componentDidMount() { this.updateStatus(); this.interval = setInterval(this.updateStatus, 2000); @@ -77,8 +97,8 @@ class AppComponent extends React.Component {
  • 1. - Run C&C Server - { this.state.completedSteps.run_server ? + Run Monkey Island Server + {this.state.completedSteps.run_server ? : ''} @@ -87,7 +107,7 @@ class AppComponent extends React.Component { 2. Run Monkey - { this.state.completedSteps.run_monkey ? + {this.state.completedSteps.run_monkey ? : ''} @@ -96,7 +116,7 @@ class AppComponent extends React.Component { 3. Infection Map - { this.state.completedSteps.infection_done ? + {this.state.completedSteps.infection_done ? : ''} @@ -105,7 +125,7 @@ class AppComponent extends React.Component { 4. Security Report - { this.state.completedSteps.report_done ? + {this.state.completedSteps.report_done ? : ''} @@ -136,14 +156,15 @@ class AppComponent extends React.Component { - ( )} /> - ( )} /> - ( )} /> - ( )} /> - ( )} /> - ( )} /> - ( )} /> - ( )} /> + ()}/> + {this.renderRoute('/', , true)} + {this.renderRoute('/configure', )} + {this.renderRoute('/run-monkey', )} + {this.renderRoute('/infection/map', )} + {this.renderRoute('/infection/telemetry', )} + {this.renderRoute('/start-over', )} + {this.renderRoute('/report', )} + {this.renderRoute('/license', )} diff --git a/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 842440149..ca3aed268 100644 --- a/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -2,8 +2,9 @@ import React from 'react'; import {Icon} from 'react-fa'; import Toggle from 'react-toggle'; import {OverlayTrigger, Tooltip} from 'react-bootstrap'; +import AuthComponent from '../../AuthComponent'; -class PreviewPaneComponent extends React.Component { +class PreviewPaneComponent extends AuthComponent { generateToolTip(text) { return ( @@ -64,7 +65,7 @@ class PreviewPaneComponent extends React.Component { forceKill(event, asset) { let newConfig = asset.config; newConfig['alive'] = !event.target.checked; - fetch('/api/monkey/' + asset.guid, + this.authFetch('/api/monkey/' + asset.guid, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, diff --git a/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 3f60ab026..afa42d6e7 100644 --- a/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -2,8 +2,9 @@ import React from 'react'; import Form from 'react-jsonschema-form'; import {Col, Nav, NavItem} from 'react-bootstrap'; import fileDownload from 'js-file-download'; +import AuthComponent from '../AuthComponent'; -class ConfigurePageComponent extends React.Component { +class ConfigurePageComponent extends AuthComponent { constructor(props) { super(props); @@ -23,7 +24,7 @@ class ConfigurePageComponent extends React.Component { } componentDidMount() { - fetch('/api/configuration') + this.authFetch('/api/configuration') .then(res => res.json()) .then(res => { let sections = []; @@ -43,7 +44,7 @@ class ConfigurePageComponent extends React.Component { onSubmit = ({formData}) => { this.currentFormData = formData; this.updateConfigSection(); - fetch('/api/configuration', + this.authFetch('/api/configuration', { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -82,7 +83,7 @@ class ConfigurePageComponent extends React.Component { }; resetConfig = () => { - fetch('/api/configuration', + this.authFetch('/api/configuration', { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -126,7 +127,7 @@ class ConfigurePageComponent extends React.Component { }; updateMonkeysRunning = () => { - fetch('/api') + this.authFetch('/api') .then(res => res.json()) .then(res => { // This check is used to prevent unnecessary re-rendering diff --git a/monkey_island/cc/ui/src/components/pages/LoginPage.js b/monkey_island/cc/ui/src/components/pages/LoginPage.js new file mode 100644 index 000000000..cc1eefecd --- /dev/null +++ b/monkey_island/cc/ui/src/components/pages/LoginPage.js @@ -0,0 +1,78 @@ +import React from 'react'; +import {Col} from 'react-bootstrap'; + +import AuthService from '../../services/AuthService' + +class LoginPageComponent extends React.Component { + login = () => { + this.auth.login(this.username, this.password).then(res => { + if (res['result']) { + this.redirectToHome(); + } else { + this.setState({failed: true}); + } + }); + }; + + updateUsername = (evt) => { + this.username = evt.target.value; + }; + + updatePassword = (evt) => { + this.password = evt.target.value; + }; + + redirectToHome = () => { + window.location.href = '/'; + }; + + constructor(props) { + super(props); + this.username = ''; + this.password = ''; + this.auth = new AuthService(); + this.state = { + failed: false + }; + if (this.auth.loggedIn()) { + this.redirectToHome(); + } + } + + render() { + return ( + +

    Login

    +
    +
    +
    + Login +
    +
    +
    + this.updateUsername(evt)}/> + this.updatePassword(evt)}/> + + { + this.state.failed ? +
    Login failed. Bad credentials.
    + : + '' + } +
    +
    +
    +
    + + ); + } +} + +export default LoginPageComponent; diff --git a/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey_island/cc/ui/src/components/pages/MapPage.js index ba5a655b1..4a54aeb8c 100644 --- a/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -6,8 +6,9 @@ import PreviewPane from 'components/map/preview-pane/PreviewPane'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {ModalContainer, ModalDialog} from 'react-modal-dialog'; import {options, edgeGroupToColor} from 'components/map/MapOptions'; +import AuthComponent from '../AuthComponent'; -class MapPageComponent extends React.Component { +class MapPageComponent extends AuthComponent { constructor(props) { super(props); this.state = { @@ -40,7 +41,7 @@ class MapPageComponent extends React.Component { }; updateMapFromServer = () => { - fetch('/api/netmap') + this.authFetch('/api/netmap') .then(res => res.json()) .then(res => { res.edges.forEach(edge => { @@ -52,7 +53,7 @@ class MapPageComponent extends React.Component { }; updateTelemetryFromServer = () => { - fetch('/api/telemetry-feed?timestamp='+this.state.telemetryLastTimestamp) + this.authFetch('/api/telemetry-feed?timestamp='+this.state.telemetryLastTimestamp) .then(res => res.json()) .then(res => { let newTelem = this.state.telemetry.concat(res['telemetries']); @@ -68,7 +69,7 @@ class MapPageComponent extends React.Component { selectionChanged(event) { if (event.nodes.length === 1) { - fetch('/api/netmap/node?id=' + event.nodes[0]) + this.authFetch('/api/netmap/node?id=' + event.nodes[0]) .then(res => res.json()) .then(res => this.setState({selected: res, selectedType: 'node'})); } @@ -80,7 +81,7 @@ class MapPageComponent extends React.Component { if (displayedEdge['group'] === 'island') { this.setState({selected: displayedEdge, selectedType: 'island_edge'}); } else { - fetch('/api/netmap/edge?id=' + event.edges[0]) + this.authFetch('/api/netmap/edge?id=' + event.edges[0]) .then(res => res.json()) .then(res => this.setState({selected: res.edge, selectedType: 'edge'})); } @@ -91,7 +92,7 @@ class MapPageComponent extends React.Component { } killAllMonkeys = () => { - fetch('/api?action=killall') + this.authFetch('/api?action=killall') .then(res => res.json()) .then(res => this.setState({killPressed: (res.status === 'OK')})); }; diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 440000596..56c2c3881 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -7,11 +7,12 @@ 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'; +import AuthComponent from '../AuthComponent'; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); let monkeyLogoImage = require('../../images/monkey-icon.svg'); -class ReportPageComponent extends React.Component { +class ReportPageComponent extends AuthComponent { Issue = { @@ -76,7 +77,7 @@ class ReportPageComponent extends React.Component { } updateMonkeysRunning = () => { - return fetch('/api') + return this.authFetch('/api') .then(res => res.json()) .then(res => { // This check is used to prevent unnecessary re-rendering @@ -89,7 +90,7 @@ class ReportPageComponent extends React.Component { }; updateMapFromServer = () => { - fetch('/api/netmap') + this.authFetch('/api/netmap') .then(res => res.json()) .then(res => { res.edges.forEach(edge => { @@ -102,7 +103,7 @@ class ReportPageComponent extends React.Component { getReportFromServer(res) { if (res['completed_steps']['run_monkey']) { - fetch('/api/report') + this.authFetch('/api/report') .then(res => res.json()) .then(res => { this.setState({ @@ -610,7 +611,7 @@ class ReportPageComponent extends React.Component { The network can probably be segmented. A monkey instance on {issue.machine} in the networks {this.generateInfoBadges(issue.networks)} - could directly access the Monkey Island C&C server in the + could directly access the Monkey Island server in the networks {this.generateInfoBadges(issue.server_networks)}.
  • diff --git a/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js b/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js index 5574a73ba..4543a5c34 100644 --- a/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js +++ b/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js @@ -3,8 +3,9 @@ import {Button, Col, Well, Nav, NavItem, Collapse} from 'react-bootstrap'; import CopyToClipboard from 'react-copy-to-clipboard'; import {Icon} from 'react-fa'; import {Link} from 'react-router-dom'; +import AuthComponent from '../AuthComponent'; -class RunMonkeyPageComponent extends React.Component { +class RunMonkeyPageComponent extends AuthComponent { constructor(props) { super(props); @@ -19,14 +20,14 @@ class RunMonkeyPageComponent extends React.Component { } componentDidMount() { - fetch('/api') + this.authFetch('/api') .then(res => res.json()) .then(res => this.setState({ ips: res['ip_addresses'], selectedIp: res['ip_addresses'][0] })); - fetch('/api/local-monkey') + this.authFetch('/api/local-monkey') .then(res => res.json()) .then(res =>{ if (res['is_running']) { @@ -36,7 +37,7 @@ class RunMonkeyPageComponent extends React.Component { } }); - fetch('/api/client-monkey') + this.authFetch('/api/client-monkey') .then(res => res.json()) .then(res => { if (res['is_running']) { @@ -60,7 +61,7 @@ class RunMonkeyPageComponent extends React.Component { } runLocalMonkey = () => { - fetch('/api/local-monkey', + this.authFetch('/api/local-monkey', { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -146,7 +147,7 @@ class RunMonkeyPageComponent extends React.Component { className="btn btn-default btn-lg center-block" disabled={this.state.runningOnIslandState !== 'not_running'} > - Run on C&C Server + Run on Monkey Island Server { this.renderIconByState(this.state.runningOnIslandState) } { diff --git a/monkey_island/cc/ui/src/components/pages/RunServerPage.js b/monkey_island/cc/ui/src/components/pages/RunServerPage.js index 5143f24fc..1ad890845 100644 --- a/monkey_island/cc/ui/src/components/pages/RunServerPage.js +++ b/monkey_island/cc/ui/src/components/pages/RunServerPage.js @@ -10,17 +10,19 @@ class RunServerPageComponent extends React.Component { render() { return ( -

    1. Monkey Island C&C Server

    +

    1. Monkey Island Server

    -

    Congrats! You have successfully set up the Monkey Island server. 👏 👏

    +

    Congrats! You have successfully set up the Monkey Island + server. 👏 👏

    The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infections. The Monkey uses various methods to propagate across a data - center and reports to this Command and Control (C&C) server. + center and reports to this Monkey Island Command and Control server.

    - To read more about the Monkey, visit infectionmonkey.com + To read more about the Monkey, visit infectionmonkey.com

    Go ahead and run the monkey. diff --git a/monkey_island/cc/ui/src/components/pages/StartOverPage.js b/monkey_island/cc/ui/src/components/pages/StartOverPage.js index 2889a7067..87716659f 100644 --- a/monkey_island/cc/ui/src/components/pages/StartOverPage.js +++ b/monkey_island/cc/ui/src/components/pages/StartOverPage.js @@ -2,8 +2,9 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import {Link} from 'react-router-dom'; import {ModalContainer, ModalDialog} from 'react-modal-dialog'; +import AuthComponent from '../AuthComponent'; -class StartOverPageComponent extends React.Component { +class StartOverPageComponent extends AuthComponent { constructor(props) { super(props); @@ -15,7 +16,7 @@ class StartOverPageComponent extends React.Component { } updateMonkeysRunning = () => { - fetch('/api') + this.authFetch('/api') .then(res => res.json()) .then(res => { // This check is used to prevent unnecessary re-rendering @@ -104,7 +105,7 @@ class StartOverPageComponent extends React.Component { this.setState({ cleaned: false }); - fetch('/api?action=reset') + this.authFetch('/api?action=reset') .then(res => res.json()) .then(res => { if (res['status'] === 'OK') { diff --git a/monkey_island/cc/ui/src/components/pages/TelemetryPage.js b/monkey_island/cc/ui/src/components/pages/TelemetryPage.js index 03c57807e..099c20a43 100644 --- a/monkey_island/cc/ui/src/components/pages/TelemetryPage.js +++ b/monkey_island/cc/ui/src/components/pages/TelemetryPage.js @@ -2,6 +2,7 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import JSONTree from 'react-json-tree' import {DataTable} from 'react-data-components'; +import AuthComponent from '../AuthComponent'; const renderJson = (val) => ; const renderTime = (val) => val.split('.')[0]; @@ -13,7 +14,7 @@ const columns = [ { title: 'Details', prop: 'data', render: renderJson, width: '40%' } ]; -class TelemetryPageComponent extends React.Component { +class TelemetryPageComponent extends AuthComponent { constructor(props) { super(props); this.state = { @@ -22,7 +23,7 @@ class TelemetryPageComponent extends React.Component { } componentDidMount = () => { - fetch('/api/telemetry') + this.authFetch('/api/telemetry') .then(res => res.json()) .then(res => this.setState({data: res.objects})); }; diff --git a/monkey_island/cc/ui/src/index.html b/monkey_island/cc/ui/src/index.html index e75b183ca..86a05dd28 100644 --- a/monkey_island/cc/ui/src/index.html +++ b/monkey_island/cc/ui/src/index.html @@ -2,7 +2,7 @@ - Infection Monkey C&C + Infection Monkey Island Server diff --git a/monkey_island/cc/ui/src/server_config/AwsConfig.js b/monkey_island/cc/ui/src/server_config/AwsConfig.js new file mode 100644 index 000000000..1c5814b5a --- /dev/null +++ b/monkey_island/cc/ui/src/server_config/AwsConfig.js @@ -0,0 +1,9 @@ +import BaseConfig from './BaseConfig'; + +class AwsConfig extends BaseConfig{ + isAuthEnabled() { + return true; + } +} + +export default AwsConfig; diff --git a/monkey_island/cc/ui/src/server_config/BaseConfig.js b/monkey_island/cc/ui/src/server_config/BaseConfig.js new file mode 100644 index 000000000..1ca82506d --- /dev/null +++ b/monkey_island/cc/ui/src/server_config/BaseConfig.js @@ -0,0 +1,8 @@ +class BaseConfig { + + isAuthEnabled() { + throw new Error('Abstract function'); + } +} + +export default BaseConfig; diff --git a/monkey_island/cc/ui/src/server_config/ServerConfig.js b/monkey_island/cc/ui/src/server_config/ServerConfig.js new file mode 100644 index 000000000..faff47abc --- /dev/null +++ b/monkey_island/cc/ui/src/server_config/ServerConfig.js @@ -0,0 +1,12 @@ +import StandardConfig from './StandardConfig'; +import AwsConfig from './AwsConfig'; + +const SERVER_CONFIG_JSON = require('json-loader!../../../server_config.json'); + +const CONFIG_DICT = + { + 'standard': StandardConfig, + 'aws': AwsConfig + }; + +export const SERVER_CONFIG = new CONFIG_DICT[SERVER_CONFIG_JSON['server_config']](); diff --git a/monkey_island/cc/ui/src/server_config/StandardConfig.js b/monkey_island/cc/ui/src/server_config/StandardConfig.js new file mode 100644 index 000000000..f549f7112 --- /dev/null +++ b/monkey_island/cc/ui/src/server_config/StandardConfig.js @@ -0,0 +1,10 @@ +import BaseConfig from './BaseConfig'; + +class StandardConfig extends BaseConfig { + + isAuthEnabled () { + return false; + } +} + +export default StandardConfig; diff --git a/monkey_island/cc/ui/src/services/AuthService.js b/monkey_island/cc/ui/src/services/AuthService.js new file mode 100644 index 000000000..c5a474ebf --- /dev/null +++ b/monkey_island/cc/ui/src/services/AuthService.js @@ -0,0 +1,106 @@ +import decode from 'jwt-decode'; +import {SERVER_CONFIG} from '../server_config/ServerConfig'; + +export default class AuthService { + AUTH_ENABLED = SERVER_CONFIG.isAuthEnabled(); + + login = (username, password) => { + if (this.AUTH_ENABLED) { + return this._login(username, password); + } else { + return {result: true}; + } + }; + + authFetch = (url, options) => { + if (this.AUTH_ENABLED) { + return this._authFetch(url, options); + } else { + return fetch(url, options); + } + }; + + _login = (username, password) => { + return this._authFetch('/api/auth', { + method: 'POST', + body: JSON.stringify({ + username, + password + }) + }).then(response => response.json()) + .then(res => { + if (res.hasOwnProperty('access_token')) { + this._setToken(res['access_token']); + return {result: true}; + } else { + this._removeToken(); + return {result: false}; + } + + }) + }; + + _authFetch = (url, options = {}) => { + const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + + if (this.loggedIn()) { + headers['Authorization'] = 'JWT ' + this._getToken(); + } + + if (options.hasOwnProperty('headers')) { + for (let header in headers) { + options['headers'][header] = headers[header]; + } + } else { + options['headers'] = headers; + } + + return fetch(url, options) + .then(res => { + if (res.status === 401) { + this._removeToken(); + } + return res; + }); + }; + + loggedIn() { + if (!this.AUTH_ENABLED) { + return true; + } + + const token = this._getToken(); + return ((token !== null) && !this._isTokenExpired(token)); + } + + logout() { + if (this.AUTH_ENABLED) { + this._removeToken(); + } + } + + _isTokenExpired(token) { + try { + return decode(token)['exp'] < Date.now() / 1000; + } + catch (err) { + return false; + } + } + + _setToken(idToken) { + localStorage.setItem('jwt', idToken); + } + + _removeToken() { + localStorage.removeItem('jwt'); + } + + _getToken() { + return localStorage.getItem('jwt') + } + +} diff --git a/monkey_island/deb-package/DEBIAN/control b/monkey_island/deb-package/DEBIAN/control index ecbd75cc6..2426feecb 100644 --- a/monkey_island/deb-package/DEBIAN/control +++ b/monkey_island/deb-package/DEBIAN/control @@ -4,5 +4,5 @@ Maintainer: Guardicore Homepage: http://www.guardicore.com Priority: optional Version: 1.0 -Description: Guardicore Infection Monkey Island (C&C) installation package +Description: Guardicore Infection Monkey Island installation package Depends: openssl, python-pip diff --git a/monkey_island/deb-package/monkey_island_pip_requirements.txt b/monkey_island/deb-package/monkey_island_pip_requirements.txt index 582efc5f2..446414ecf 100644 --- a/monkey_island/deb-package/monkey_island_pip_requirements.txt +++ b/monkey_island/deb-package/monkey_island_pip_requirements.txt @@ -8,6 +8,7 @@ click flask Flask-Pymongo Flask-Restful +Flask-JWT jsonschema netifaces ipaddress diff --git a/monkey_island/readme.txt b/monkey_island/readme.txt index 2feaaa1ab..320f5caa3 100644 --- a/monkey_island/readme.txt +++ b/monkey_island/readme.txt @@ -1,4 +1,4 @@ -How to set C&C server: +How to set up the Monkey Island server: ---------------- On Windows ----------------: 1. Create folder "bin" under monkey_island @@ -18,7 +18,7 @@ How to set C&C server: 5.1. Download and install from: https://go.microsoft.com/fwlink/?LinkId=746572 6. Generate SSL Certificate 6.1. run create_certificate.bat when your current working directory is monkey_island -7. Create the monkey_island\cc\binaries folder and put chaos monkey binaries inside +7. Create the monkey_island\cc\binaries folder and put Infection Monkey binaries inside monkey-linux-64 - monkey binary for linux 64bit monkey-linux-32 - monkey binary for linux 32bit monkey-windows-32.exe - monkey binary for windows 32bit diff --git a/monkey_island/requirements.txt b/monkey_island/requirements.txt index 18098eec0..a090a3b4b 100644 --- a/monkey_island/requirements.txt +++ b/monkey_island/requirements.txt @@ -8,6 +8,7 @@ click flask Flask-Pymongo Flask-Restful +Flask-JWT jsonschema netifaces ipaddress