diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index a8c0687fc..e788bb36c 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -546,6 +546,38 @@ fullTest.conf is a good config to start, because it covers all machines. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 12 Tunneling M4

+

(10.2.0.12)

(Exploitable)
OS:Windows server 2019 x64
Default service’s port:445
Root password:t67TC5ZDmz
Server’s config:Default
Notes:Accessible only trough Nr.10
+ diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 7a75c1b7e..7dd61cd19 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -5,7 +5,7 @@ requests odict paramiko psutil -PyInstaller +git+https://github.com/guardicore/pyinstaller ecdsa netifaces ipaddress diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index ae7a6934c..20b481f31 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -7,9 +7,12 @@ import urllib from logging import getLogger from urllib.parse import urlsplit +import requests + import infection_monkey.monkeyfs as monkeyfs from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time from infection_monkey.network.tools import get_interface_to_target +import infection_monkey.control __author__ = 'hoffer' @@ -108,12 +111,35 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): timeout = 30 # timeout with clients, set to None not to make persistent connection proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header - protocol_version = "HTTP/1.1" + + def do_POST(self): + try: + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode() + LOG.info("Received bootloader's request: {}".format(post_data)) + try: + dest_path = self.path + r = requests.post(url=dest_path, + data=post_data, + verify=False, + proxies=infection_monkey.control.ControlClient.proxies) + self.send_response(r.status_code) + except requests.exceptions.ConnectionError as e: + LOG.error("Couldn't forward request to the island: {}".format(e)) + self.send_response(404) + except Exception as e: + LOG.error("Failed to forward bootloader request: {}".format(e)) + finally: + self.end_headers() + self.wfile.write(r.content) + except Exception as e: + LOG.error("Failed receiving bootloader telemetry: {}".format(e)) def version_string(self): return "" def do_CONNECT(self): + LOG.info("Received a connect request!") # just provide a tunnel, transfer the data with no modification req = self req.path = "https://%s/" % req.path.replace(':443', '') diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 2af0e2230..3a1134930 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -19,6 +19,7 @@ from monkey_island.cc.resources.island_configuration import IslandConfiguration from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node +from monkey_island.cc.resources.node_states import NodeStates from monkey_island.cc.resources.remote_run import RemoteRun from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.root import Root @@ -30,6 +31,7 @@ from monkey_island.cc.resources.version_update import VersionUpdate from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.attack.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_report import AttackReport +from monkey_island.cc.resources.bootloader import Bootloader from monkey_island.cc.services.database import Database from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.representations import output_json @@ -87,6 +89,7 @@ def init_app_url_rules(app): def init_api_resources(api): api.add_resource(Root, '/api') api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/') + api.add_resource(Bootloader, '/api/bootloader/') api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/') api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/') api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') @@ -97,6 +100,7 @@ def init_api_resources(api): api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') + api.add_resource(NodeStates, '/api/netmap/nodeStates') # report_type: zero_trust or security api.add_resource( diff --git a/monkey/monkey_island/cc/bootloader_server.py b/monkey/monkey_island/cc/bootloader_server.py new file mode 100644 index 000000000..5adfb3d22 --- /dev/null +++ b/monkey/monkey_island/cc/bootloader_server.py @@ -0,0 +1,49 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn +from urllib import parse +import urllib3 +import logging + +import requests +import pymongo + +from monkey_island.cc.environment import Environment + +# Disable "unverified certificate" warnings when sending requests to island +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +logger = logging.getLogger(__name__) + + +class BootloaderHttpServer(ThreadingMixIn, HTTPServer): + + def __init__(self, mongo_url): + self.mongo_client = pymongo.MongoClient(mongo_url) + server_address = ('', 5001) + super().__init__(server_address, BootloaderHTTPRequestHandler) + + +class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler): + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode() + island_server_path = BootloaderHTTPRequestHandler.get_bootloader_resource_url(self.request.getsockname()[0]) + island_server_path = parse.urljoin(island_server_path, self.path[1:]) + r = requests.post(url=island_server_path, data=post_data, verify=False) + + try: + if r.status_code != 200: + self.send_response(404) + else: + self.send_response(200) + self.end_headers() + self.wfile.write(r.content) + except Exception as e: + logger.error("Failed to respond to bootloader: {}".format(e)) + finally: + self.connection.close() + + @staticmethod + def get_bootloader_resource_url(server_ip): + return "https://" + server_ip + ":" + str(Environment._ISLAND_PORT) + "/api/bootloader/" + diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index f06d36ea3..f02aebafa 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -3,6 +3,7 @@ import os.path import sys import time import logging +from threading import Thread MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0" @@ -26,9 +27,20 @@ from monkey_island.cc.environment.environment import env from monkey_island.cc.database import is_db_server_up, get_db_version from monkey_island.cc.resources.monkey_download import MonkeyDownload from common.version import get_version +from monkey_island.cc.bootloader_server import BootloaderHttpServer def main(): + logger.info("Starting bootloader server") + mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) + bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True) + + bootloader_server_thread.start() + start_island_server() + bootloader_server_thread.join() + + +def start_island_server(): from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop diff --git a/monkey/monkey_island/cc/resources/bootloader.py b/monkey/monkey_island/cc/resources/bootloader.py new file mode 100644 index 000000000..733b35651 --- /dev/null +++ b/monkey/monkey_island/cc/resources/bootloader.py @@ -0,0 +1,38 @@ +import json +from typing import Dict + +import flask_restful +from flask import request, make_response + +from monkey_island.cc.services.bootloader import BootloaderService + + +class Bootloader(flask_restful.Resource): + + # Used by monkey. can't secure. + def post(self, os): + if os == 'linux': + data = Bootloader._get_request_contents_linux(request.data) + elif os == 'windows': + data = Bootloader._get_request_contents_windows(request.data) + else: + return make_response({"status": "OS_NOT_FOUND"}, 404) + + result = BootloaderService.parse_bootloader_telem(data) + + if result: + return make_response({"status": "RUN"}, 200) + else: + return make_response({"status": "ABORT"}, 200) + + @staticmethod + def _get_request_contents_linux(request_data: bytes) -> Dict[str, str]: + parsed_data = json.loads(request_data.decode().replace("\"\n", "") + .replace("\n", "") + .replace("NAME=\"", "") + .replace("\":\",", "\":\"\",")) + return parsed_data + + @staticmethod + def _get_request_contents_windows(request_data: bytes) -> Dict[str, str]: + return json.loads(request_data.decode("utf-16", "ignore")) diff --git a/monkey/monkey_island/cc/resources/bootloader_test.py b/monkey/monkey_island/cc/resources/bootloader_test.py new file mode 100644 index 000000000..41ce9304c --- /dev/null +++ b/monkey/monkey_island/cc/resources/bootloader_test.py @@ -0,0 +1,56 @@ +from unittest import TestCase + +from monkey_island.cc.resources.bootloader import Bootloader +from monkey_island.cc.services.utils.bootloader_config import SUPPORTED_WINDOWS_VERSIONS + + +class TestBootloader(TestCase): + + def test_get_request_contents_linux(self): + data_without_tunnel = b'{"system":"linux", ' \ + b'"os_version":"NAME="Ubuntu"\n", ' \ + b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' \ + b'"hostname":"test-TEST", ' \ + b'"tunnel":false, ' \ + b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' + data_with_tunnel = b'{"system":"linux", ' \ + b'"os_version":"NAME="Ubuntu"\n", ' \ + b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' \ + b'"hostname":"test-TEST", ' \ + b'"tunnel":"192.168.56.1:5002", ' \ + b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' + + result1 = Bootloader._get_request_contents_linux(data_without_tunnel) + self.assertTrue(result1['system'] == "linux") + self.assertTrue(result1['os_version'] == "Ubuntu") + self.assertTrue(result1['glibc_version'] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") + self.assertTrue(result1['hostname'] == "test-TEST") + self.assertFalse(result1['tunnel']) + self.assertTrue(result1['ips'] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) + + result2 = Bootloader._get_request_contents_linux(data_with_tunnel) + self.assertTrue(result2['system'] == "linux") + self.assertTrue(result2['os_version'] == "Ubuntu") + self.assertTrue(result2['glibc_version'] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") + self.assertTrue(result2['hostname'] == "test-TEST") + self.assertTrue(result2['tunnel'] == "192.168.56.1:5002") + self.assertTrue(result2['ips'] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) + + def test_get_request_contents_windows(self): + windows_data = b'{\x00"\x00s\x00y\x00s\x00t\x00e\x00m\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o' \ + b'\x00w\x00s\x00"\x00,\x00 \x00"\x00o\x00s\x00_\x00v\x00e\x00r\x00s\x00i\x00o\x00n' \ + b'\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o\x00w\x00s\x008\x00_\x00o\x00r\x00_\x00g\x00r' \ + b'\x00e\x00a\x00t\x00e\x00r\x00"\x00,\x00 \x00"\x00h\x00o\x00s\x00t\x00n\x00a\x00m\x00e\x00"' \ + b'\x00:\x00"\x00D\x00E\x00S\x00K\x00T\x00O\x00P\x00-\x00P\x00J\x00H\x00U\x003\x006\x00B\x00"' \ + b'\x00,\x00 \x00"\x00t\x00u\x00n\x00n\x00e\x00l\x00"\x00:\x00f\x00a\x00l\x00s\x00e\x00,\x00 ' \ + b'\x00"\x00i\x00p\x00s\x00"\x00:\x00 \x00[\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x005' \ + b'\x006\x00.\x001\x00"\x00,\x00 \x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x004\x009' \ + b'\x00.\x001\x00"\x00,\x00 \x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x001\x007\x00.' \ + b'\x001\x00"\x00]\x00}\x00' + + result = Bootloader._get_request_contents_windows(windows_data) + self.assertTrue(result['system'] == "windows") + self.assertTrue(result['os_version'] == "windows8_or_greater") + self.assertTrue(result['hostname'] == "DESKTOP-PJHU36B") + self.assertFalse(result['tunnel']) + self.assertTrue(result['ips'] == ["192.168.56.1", "192.168.249.1", "192.168.217.1"]) diff --git a/monkey/monkey_island/cc/resources/node_states.py b/monkey/monkey_island/cc/resources/node_states.py new file mode 100644 index 000000000..c7c99cc94 --- /dev/null +++ b/monkey/monkey_island/cc/resources/node_states.py @@ -0,0 +1,11 @@ +from flask import request +import flask_restful + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.utils.node_states import NodeStates as NodeStateList + + +class NodeStates(flask_restful.Resource): + @jwt_required() + def get(self): + return {'node_states': [state.value for state in NodeStateList]} diff --git a/monkey/monkey_island/cc/services/bootloader.py b/monkey/monkey_island/cc/services/bootloader.py new file mode 100644 index 000000000..49dbe3154 --- /dev/null +++ b/monkey/monkey_island/cc/services/bootloader.py @@ -0,0 +1,69 @@ +from typing import Dict, List + +from bson import ObjectId + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.node import NodeService, NodeCreationException +from monkey_island.cc.services.utils.node_states import NodeStates +from monkey_island.cc.services.utils.bootloader_config import SUPPORTED_WINDOWS_VERSIONS, MIN_GLIBC_VERSION + + +class BootloaderService: + + @staticmethod + def parse_bootloader_telem(telem: Dict) -> bool: + telem['ips'] = BootloaderService.remove_local_ips(telem['ips']) + if telem['os_version'] == "": + telem['os_version'] = "Unknown OS" + + telem_id = BootloaderService.get_mongo_id_for_bootloader_telem(telem) + mongo.db.bootloader_telems.update({'_id': telem_id}, telem, upsert=True) + + will_monkey_run = BootloaderService.is_os_compatible(telem) + try: + node = NodeService.get_or_create_node_from_bootloader_telem(telem, will_monkey_run) + except NodeCreationException: + # Didn't find the node, but allow monkey to run anyways + return True + + node_group = BootloaderService.get_next_node_state(node, telem['system'], will_monkey_run) + if 'group' not in node or node['group'] != node_group.value: + NodeService.set_node_group(node['_id'], node_group) + return will_monkey_run + + @staticmethod + def get_next_node_state(node: Dict, system: str, will_monkey_run: bool) -> NodeStates: + group_keywords = [system, 'monkey'] + if 'group' in node and node['group'] == 'island': + group_keywords.extend(['island', 'starting']) + else: + group_keywords.append('starting') if will_monkey_run else group_keywords.append('old') + node_group = NodeStates.get_by_keywords(group_keywords) + return node_group + + @staticmethod + def get_mongo_id_for_bootloader_telem(bootloader_telem) -> ObjectId: + ip_hash = hex(hash(str(bootloader_telem['ips'])))[3:15] + hostname_hash = hex(hash(bootloader_telem['hostname']))[3:15] + return ObjectId(ip_hash + hostname_hash) + + @staticmethod + def is_os_compatible(bootloader_data) -> bool: + if bootloader_data['system'] == 'windows': + return BootloaderService.is_windows_version_supported(bootloader_data['os_version']) + elif bootloader_data['system'] == 'linux': + return BootloaderService.is_glibc_supported(bootloader_data['glibc_version']) + + @staticmethod + def is_windows_version_supported(windows_version) -> bool: + return SUPPORTED_WINDOWS_VERSIONS.get(windows_version, True) + + @staticmethod + def is_glibc_supported(glibc_version_string) -> bool: + glibc_version_string = glibc_version_string.lower() + glibc_version = glibc_version_string.split(' ')[-1] + return glibc_version >= str(MIN_GLIBC_VERSION) and 'eglibc' not in glibc_version_string + + @staticmethod + def remove_local_ips(ip_list) -> List[str]: + return [i for i in ip_list if not i.startswith("127")] diff --git a/monkey/monkey_island/cc/services/bootloader_test.py b/monkey/monkey_island/cc/services/bootloader_test.py new file mode 100644 index 000000000..03df2be97 --- /dev/null +++ b/monkey/monkey_island/cc/services/bootloader_test.py @@ -0,0 +1,35 @@ +from unittest import TestCase + +from monkey_island.cc.services.bootloader import BootloaderService + +WINDOWS_VERSIONS = { + "5.0": "Windows 2000", + "5.1": "Windows XP", + "5.2": "Windows XP/server 2003", + "6.0": "Windows Vista/server 2008", + "6.1": "Windows 7/server 2008R2", + "6.2": "Windows 8/server 2012", + "6.3": "Windows 8.1/server 2012R2", + "10.0": "Windows 10/server 2016-2019" +} + +MIN_GLIBC_VERSION = 2.14 + + +class TestBootloaderService(TestCase): + + def test_is_glibc_supported(self): + str1 = "ldd (Ubuntu EGLIBC 2.15-0ubuntu10) 2.15" + str2 = "ldd (GNU libc) 2.12" + str3 = "ldd (GNU libc) 2.28" + str4 = "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23" + self.assertTrue(not BootloaderService.is_glibc_supported(str1) and + not BootloaderService.is_glibc_supported(str2) and + BootloaderService.is_glibc_supported(str3) and + BootloaderService.is_glibc_supported(str4)) + + def test_remove_local_ips(self): + ips = ["127.1.1.1", "127.0.0.1", "192.168.56.1"] + ips = BootloaderService.remove_local_ips(ips) + self.assertEqual(["192.168.56.1"], ips) + diff --git a/monkey/monkey_island/cc/services/edge.py b/monkey/monkey_island/cc/services/edge.py index d5e38768d..a8016f5a8 100644 --- a/monkey/monkey_island/cc/services/edge.py +++ b/monkey/monkey_island/cc/services/edge.py @@ -2,7 +2,7 @@ from bson import ObjectId from monkey_island.cc.database import mongo import monkey_island.cc.services.node -from monkey_island.cc.models import Monkey +from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError __author__ = "itay.mizeretz" @@ -145,7 +145,10 @@ class EdgeService: from_id = edge["from"] to_id = edge["to"] - from_label = Monkey.get_label_by_id(from_id) + try: + from_label = Monkey.get_label_by_id(from_id) + except MonkeyNotFoundError: + from_label = node_service.get_node_by_id(from_id)['domain_name'] if to_id == ObjectId("000000000000000000000000"): to_label = 'MonkeyIsland' diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 3109f7a78..3ad0a270b 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -1,4 +1,6 @@ from datetime import datetime, timedelta +from typing import Dict +import socket from bson import ObjectId @@ -6,9 +8,9 @@ import monkey_island.cc.services.log from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.edge import EdgeService -from monkey_island.cc.utils import local_ip_addresses -import socket +from monkey_island.cc.utils import local_ip_addresses, is_local_ips from monkey_island.cc import models +from monkey_island.cc.services.utils.node_states import NodeStates __author__ = "itay.mizeretz" @@ -122,20 +124,25 @@ class NodeService: @staticmethod def get_monkey_group(monkey): + keywords = [] if len(set(monkey["ip_addresses"]).intersection(local_ip_addresses())) != 0: - monkey_type = "island_monkey" + keywords.extend(["island", "monkey"]) else: monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey" + keywords.append(monkey_type) - monkey_os = NodeService.get_monkey_os(monkey) - monkey_running = "" if Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead() else "_running" - return "%s_%s%s" % (monkey_type, monkey_os, monkey_running) + keywords.append(NodeService.get_monkey_os(monkey)) + if not Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead(): + keywords.append("running") + return NodeStates.get_by_keywords(keywords).value @staticmethod - def get_node_group(node): + def get_node_group(node) -> str: + if 'group' in node and node['group']: + return node['group'] node_type = "exploited" if node.get("exploited") else "clean" node_os = NodeService.get_node_os(node) - return "%s_%s" % (node_type, node_os) + return NodeStates.get_by_keywords([node_type, node_os]).value @staticmethod def monkey_to_net_node(monkey, for_report=False): @@ -166,6 +173,12 @@ class NodeService: "os": NodeService.get_node_os(node) } + @staticmethod + def set_node_group(node_id: str, node_group: NodeStates): + mongo.db.node.update({"_id": node_id}, + {'$set': {'group': node_group.value}}, + upsert=False) + @staticmethod def unset_all_monkey_tunnels(monkey_id): mongo.db.monkey.update( @@ -207,6 +220,43 @@ class NodeService: }) return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) + @staticmethod + def create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool): + new_node_insert_result = mongo.db.node.insert_one( + { + "ip_addresses": bootloader_telem['ips'], + "domain_name": bootloader_telem['hostname'], + "will_monkey_run": will_monkey_run, + "exploited": False, + "creds": [], + "os": + { + "type": bootloader_telem['system'], + "version": bootloader_telem['os_version'] + } + }) + return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) + + @staticmethod + def get_or_create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool) -> Dict: + if is_local_ips(bootloader_telem['ips']): + raise NodeCreationException("Bootloader ran on island, no need to create new node.") + + new_node = mongo.db.node.find_one({"ip_addresses": {"$in": bootloader_telem['ips']}}) + if new_node is None: + new_node = NodeService.create_node_from_bootloader_telem(bootloader_telem, will_monkey_run) + if bootloader_telem['tunnel']: + dst_node = NodeService.get_node_or_monkey_by_ip(bootloader_telem['tunnel']) + else: + dst_node = NodeService.get_monkey_island_node() + edge = EdgeService.get_or_create_edge(new_node['_id'], dst_node['id']) + mongo.db.edge.update({"_id": edge["_id"]}, + {'$set': {'tunnel': bool(bootloader_telem['tunnel']), + 'ip_address': bootloader_telem['ips'][0], + 'group': NodeStates.get_by_keywords(['island']).value}}, + upsert=False) + return new_node + @staticmethod def get_or_create_node(ip_address, domain_name=''): new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) @@ -354,3 +404,6 @@ class NodeService: @staticmethod def get_hostname_by_id(node_id): return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) + +class NodeCreationException(Exception): + pass diff --git a/monkey/monkey_island/cc/services/utils/bootloader_config.py b/monkey/monkey_island/cc/services/utils/bootloader_config.py new file mode 100644 index 000000000..cb9ff04a6 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/bootloader_config.py @@ -0,0 +1,12 @@ +MIN_GLIBC_VERSION = 2.14 + +SUPPORTED_WINDOWS_VERSIONS = { + "xp_or_lower": False, + "vista": False, + "vista_sp1": False, + "vista_sp2": True, + "windows7": True, + "windows7_sp1": True, + "windows8_or_greater": True, +} + diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py new file mode 100644 index 000000000..db8dd6429 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from enum import Enum +from typing import List +import collections + + +class NodeStates(Enum): + CLEAN_UNKNOWN = 'clean_unknown' + CLEAN_LINUX = 'clean_linux' + CLEAN_WINDOWS = 'clean_windows' + EXPLOITED_LINUX = 'exploited_linux' + EXPLOITED_WINDOWS = 'exploited_windows' + ISLAND = 'island' + ISLAND_MONKEY_LINUX = 'island_monkey_linux' + ISLAND_MONKEY_LINUX_RUNNING = 'island_monkey_linux_running' + ISLAND_MONKEY_LINUX_STARTING = 'island_monkey_linux_starting' + ISLAND_MONKEY_WINDOWS = 'island_monkey_windows' + ISLAND_MONKEY_WINDOWS_RUNNING = 'island_monkey_windows_running' + ISLAND_MONKEY_WINDOWS_STARTING = 'island_monkey_windows_starting' + MANUAL_LINUX = 'manual_linux' + MANUAL_LINUX_RUNNING = 'manual_linux_running' + MANUAL_WINDOWS = 'manual_windows' + MANUAL_WINDOWS_RUNNING = 'manual_windows_running' + MONKEY_LINUX = 'monkey_linux' + MONKEY_LINUX_RUNNING = 'monkey_linux_running' + MONKEY_WINDOWS = 'monkey_windows' + MONKEY_WINDOWS_RUNNING = 'monkey_windows_running' + MONKEY_WINDOWS_STARTING = 'monkey_windows_starting' + MONKEY_LINUX_STARTING = 'monkey_linux_starting' + MONKEY_WINDOWS_OLD = 'monkey_windows_old' + MONKEY_LINUX_OLD = 'monkey_linux_old' + + @staticmethod + def get_by_keywords(keywords: List) -> NodeStates: + potential_groups = [i for i in NodeStates if NodeStates._is_state_from_keywords(i, keywords)] + if len(potential_groups) > 1: + raise MultipleGroupsFoundException("Multiple groups contain provided keywords. " + "Manually build group string to ensure keyword order.") + elif len(potential_groups) == 0: + raise NoGroupsFoundException("No groups found with provided keywords. " + "Check for typos and make sure group codes want to find exists.") + return potential_groups[0] + + @staticmethod + def _is_state_from_keywords(group, keywords) -> bool: + group_keywords = group.value.split("_") + return collections.Counter(group_keywords) == collections.Counter(keywords) + + +class MultipleGroupsFoundException(Exception): + pass + + +class NoGroupsFoundException(Exception): + pass diff --git a/monkey/monkey_island/cc/services/utils/node_states_test.py b/monkey/monkey_island/cc/services/utils/node_states_test.py new file mode 100644 index 000000000..7a8b7dfd1 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/node_states_test.py @@ -0,0 +1,15 @@ +from unittest import TestCase + +from monkey_island.cc.services.utils.node_states import NodeStates, NoGroupsFoundException + + +class TestNodeGroups(TestCase): + + def test_get_group_by_keywords(self): + self.assertEqual(NodeStates.get_by_keywords(['island']), NodeStates.ISLAND) + self.assertEqual(NodeStates.get_by_keywords(['running', 'linux', 'monkey']), NodeStates.MONKEY_LINUX_RUNNING) + self.assertEqual(NodeStates.get_by_keywords(['monkey', 'linux', 'running']), NodeStates.MONKEY_LINUX_RUNNING) + with self.assertRaises(NoGroupsFoundException): + NodeStates.get_by_keywords(['bogus', 'values', 'from', 'long', 'list', 'should', 'fail']) + + diff --git a/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js index 7e4805797..742d061f4 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js @@ -1,16 +1,11 @@ -const groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island', - 'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running', - 'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux', - 'monkey_linux_running', 'monkey_windows', 'monkey_windows_running']; - -let getGroupsOptions = () => { +let getGroupsOptions = (stateList) => { let groupOptions = {}; - for (let groupName of groupNames) { - groupOptions[groupName] = + for (let stateName of stateList) { + groupOptions[stateName] = { shape: 'image', size: 50, - image: require('../../images/nodes/' + groupName + '.png') + image: require('../../images/nodes/' + stateName + '.png') }; } @@ -52,11 +47,11 @@ export const basic_options = { } }; -export const options = (() => { +export function getOptions(stateList) { let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ - opts.groups = getGroupsOptions(); + opts.groups = getGroupsOptions(stateList); return opts; -})(); +} export const optionsPth = (() => { let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js deleted file mode 100644 index a9fa7a6d6..000000000 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js +++ /dev/null @@ -1,246 +0,0 @@ -import React from 'react'; -import Toggle from 'react-toggle'; -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import download from 'downloadjs' -import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; - -class InfMapPreviewPaneComponent extends PreviewPaneComponent { - - osRow(asset) { - return ( - - - - - ); - } - - ipsRow(asset) { - return ( - - - - - ); - } - - servicesRow(asset) { - return ( - - - - - ); - } - - accessibleRow(asset) { - return ( - - - - - ); - } - - statusRow(asset) { - return ( - - - - - ); - } - - forceKill(event, asset) { - let newConfig = asset.config; - newConfig['alive'] = !event.target.checked; - this.authFetch('/api/monkey/' + asset.guid, - { - method: 'PATCH', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({config: newConfig}) - }); - } - - forceKillRow(asset) { - return ( - - - - - ); - } - - unescapeLog(st) { - return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string. - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\t/g, '\t') - .replace(/\\b/g, '\b') - .replace(/\\f/g, '\f') - .replace(/\\"/g, '\"') - .replace(/\\'/g, '\'') - .replace(/\\&/g, '\&'); - } - - downloadLog(asset) { - this.authFetch('/api/log?id=' + asset.id) - .then(res => res.json()) - .then(res => { - let timestamp = res['timestamp']; - timestamp = timestamp.substr(0, timestamp.indexOf('.')); - let filename = res['monkey_label'].split(':').join('-') + ' - ' + timestamp + '.log'; - let logContent = this.unescapeLog(res['log']); - download(logContent, filename, 'text/plain'); - }); - - } - - downloadLogRow(asset) { - return ( - - - - - ); - } - - exploitsTimeline(asset) { - if (asset.exploits.length === 0) { - return (
); - } - - return ( -
-

- Exploit Timeline  - {this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')} -

- -
- ) - } - - assetInfo(asset) { - return ( -
-
Operating System{asset.os.charAt(0).toUpperCase() + asset.os.slice(1)}
IP Addresses{asset.ip_addresses.map(val =>
{val}
)}
Services{asset.services.map(val =>
{val}
)}
- Accessible From  - {this.generateToolTip('List of machine which can access this one using a network protocol')} - {asset.accessible_from_nodes.map(val =>
{val}
)}
Status{(asset.dead) ? 'Dead' : 'Alive'}
- Force Kill  - {this.generateToolTip('If this is on, monkey will die next time it communicates')} - - this.forceKill(e, asset)}/> - -
- Download Log - - this.downloadLog(asset)}>Download -
- - {this.osRow(asset)} - {this.ipsRow(asset)} - {this.servicesRow(asset)} - {this.accessibleRow(asset)} - -
- {this.exploitsTimeline(asset)} - - ); - } - - infectedAssetInfo(asset) { - return ( -
- - - {this.osRow(asset)} - {this.statusRow(asset)} - {this.ipsRow(asset)} - {this.servicesRow(asset)} - {this.accessibleRow(asset)} - {this.forceKillRow(asset)} - {this.downloadLogRow(asset)} - -
- {this.exploitsTimeline(asset)} -
- ); - } - - scanInfo(edge) { - return ( -
- - - - - - - - - - - - - - - -
Operating System{edge.os.type}
IP Address{edge.ip_address}
Services{edge.services.map(val =>
{val}
)}
- { - (edge.exploits.length === 0) ? - '' : -
-

Timeline

- -
- } -
- ); - } - - islandEdgeInfo() { - return ( -
-
- ); - } - - getInfoByProps() { - switch (this.props.type) { - case 'edge': - return this.scanInfo(this.props.item); - case 'node': - return this.props.item.group.includes('monkey', 'manual') ? - this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item); - case 'island_edge': - return this.islandEdgeInfo(); - } - - return null; - } -} - -export default InfMapPreviewPaneComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index a5d4d98fa..dd99c8503 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -253,8 +253,15 @@ class PreviewPaneComponent extends AuthComponent { info = this.scanInfo(this.props.item); break; case 'node': - info = this.props.item.group.includes('monkey', 'manual') ? this.infectedAssetInfo(this.props.item) : - this.props.item.group !== 'island' ? this.assetInfo(this.props.item) : this.islandAssetInfo(); + if (this.props.item.group.includes('monkey') && this.props.item.group.includes('starting')) { + info = this.assetInfo(this.props.item); + } else if (this.props.item.group.includes('monkey', 'manual')) { + info = this.infectedAssetInfo(this.props.item) + } else if (this.props.item.group !== 'island') { + info = this.assetInfo(this.props.item) + } else { + info = this.islandAssetInfo(); + } break; case 'island_edge': info = this.islandEdgeInfo(); diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js deleted file mode 100644 index a5ffa3020..000000000 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; - -class PthPreviewPaneComponent extends PreviewPaneComponent { - nodeInfo(asset) { - return ( -
- - - - - - - - - - - - - - - - - - - -
Hostname{asset.hostname}
IP Addresses{asset.ips.map(val =>
{val}
)}
Services{asset.services.map(val =>
{val}
)}
Compromised Users{asset.users.map(val =>
{val}
)}
-
- ); - } - - edgeInfo(edge) { - return ( -
- - - - - - - -
Compromised Users{edge.users.map(val =>
{val}
)}
-
- ); - } - - getInfoByProps() { - switch (this.props.type) { - case 'edge': - return this.edgeInfo(this.props.item); - case 'node': - return this.nodeInfo(this.props.item); - } - - return null; - } -} - -export default PthPreviewPaneComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index f75091270..3839e55fd 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -3,9 +3,9 @@ import {Col, Modal} from 'react-bootstrap'; import {Link} from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faStopCircle, faMinus } from '@fortawesome/free-solid-svg-icons' -import InfMapPreviewPaneComponent from 'components/map/preview-pane/InfMapPreviewPane'; +import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {options, edgeGroupToColor} from 'components/map/MapOptions'; +import {getOptions, edgeGroupToColor} from 'components/map/MapOptions'; import AuthComponent from '../AuthComponent'; class MapPageComponent extends AuthComponent { @@ -13,6 +13,7 @@ class MapPageComponent extends AuthComponent { super(props); this.state = { graph: {nodes: [], edges: []}, + nodeStateList:[], selected: null, selectedType: null, killPressed: false, @@ -33,6 +34,7 @@ class MapPageComponent extends AuthComponent { }; componentDidMount() { + this.getNodeStateListFromServer(); this.updateMapFromServer(); this.interval = setInterval(this.timedEvents, 5000); } @@ -41,6 +43,14 @@ class MapPageComponent extends AuthComponent { clearInterval(this.interval); } + getNodeStateListFromServer = () => { + this.authFetch('/api/netmap/nodeStates') + .then(res => res.json()) + .then(res => { + this.setState({nodeStateList: res.node_states}); + }); + }; + timedEvents = () => { this.updateMapFromServer(); this.updateTelemetryFromServer(); @@ -201,7 +211,7 @@ class MapPageComponent extends AuthComponent { {this.renderTelemetryConsole()}
- +
{this.renderTelemetryLineCount()} @@ -226,7 +236,7 @@ class MapPageComponent extends AuthComponent { : ''} - + ); diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 451b69213..909659e87 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -3,8 +3,6 @@ import '../../styles/report/ReportPage.scss'; import React from 'react'; import {Route} from 'react-router-dom'; import {Col, Nav, NavItem} from 'react-bootstrap'; -import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {edgeGroupToColor, options} from 'components/map/MapOptions'; import AuthComponent from '../AuthComponent'; import MustRunMonkeyWarning from '../report-components/common/MustRunMonkeyWarning'; import AttackReport from '../report-components/AttackReport' diff --git a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js index c175becd3..3199385ba 100644 --- a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js +++ b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js @@ -3,10 +3,18 @@ import Graph from 'react-graph-vis'; import Dimensions from 'react-dimensions' class GraphWrapper extends React.Component { + + constructor(props) { + super(props); + } + render() { - let newOptions = this.props.options; - newOptions.height = this.props.containerHeight.toString() + 'px'; - newOptions.width = this.props.containerWidth.toString() + 'px'; + let newOptions = null; + if(this.props.options !== undefined){ + newOptions = this.props.options; + newOptions.height = this.props.containerHeight.toString() + 'px'; + newOptions.width = this.props.containerWidth.toString() + 'px'; + } return () } } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index b44f367e1..63bde5203 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -3,7 +3,7 @@ import BreachedServers from 'components/report-components/security/BreachedServe import ScannedServers from 'components/report-components/security/ScannedServers'; import PostBreach from 'components/report-components/security/PostBreach'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {edgeGroupToColor, options} from 'components/map/MapOptions'; +import {edgeGroupToColor, getOptions} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/security/StolenPasswords'; import CollapsibleWellComponent from 'components/report-components/security/CollapsibleWell'; import {Line} from 'rc-progress'; @@ -54,14 +54,24 @@ class ReportPageComponent extends AuthComponent { super(props); this.state = { report: props.report, - graph: {nodes: [], edges: []} + graph: {nodes: [], edges: []}, + nodeStateList: [] }; } componentDidMount() { + this.getNodeStateListFromServer(); this.updateMapFromServer(); } + getNodeStateListFromServer = () => { + this.authFetch('/api/netmap/nodeStates') + .then(res => res.json()) + .then(res => { + this.setState({nodeStateList: res.node_states}); + }); + }; + componentWillUnmount() { clearInterval(this.interval); } @@ -396,7 +406,7 @@ class ReportPageComponent extends AuthComponent { Island Communication
- +
diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png new file mode 100644 index 000000000..cd53d7cb7 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png new file mode 100644 index 000000000..67129b5cc Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png new file mode 100644 index 000000000..d582c8ecf Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png new file mode 100644 index 000000000..a7aa12793 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png new file mode 100644 index 000000000..58de7ff52 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png new file mode 100644 index 000000000..1545280b4 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png differ diff --git a/monkey/monkey_island/cc/utils.py b/monkey/monkey_island/cc/utils.py index 01c69e648..5504c34b6 100644 --- a/monkey/monkey_island/cc/utils.py +++ b/monkey/monkey_island/cc/utils.py @@ -1,5 +1,7 @@ import socket import sys +from typing import List +import collections import array @@ -49,6 +51,11 @@ else: return result +def is_local_ips(ips: List) -> bool: + filtered_local_ips = [ip for ip in local_ip_addresses() if not ip.startswith('169.254')] + return collections.Counter(ips) == collections.Counter(filtered_local_ips) + + # The local IP addresses list should not change often. Therefore, we can cache the result and never call this function # more than once. This stopgap measure is here since this function is called a lot of times during the report # generation.