diff --git a/infection_monkey/config.py b/infection_monkey/config.py index 8e00c8bfc..4f7f4bcd9 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -186,6 +186,7 @@ class Configuration(object): local_network_scan = True subnet_scan_list = [] + inaccessible_subnets = [] blocked_ips = [] diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index e4ed17b8f..4e608f72f 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -10,6 +10,7 @@ "subnet_scan_list": [ ], + "inaccessible_subnets": [], "blocked_ips": [], "current_server": "192.0.2.0:5000", "alive": true, @@ -96,4 +97,4 @@ "use_file_logging": true, "victims_max_exploit": 7, "victims_max_find": 30 -} \ No newline at end of file +} diff --git a/infection_monkey/network/network_scanner.py b/infection_monkey/network/network_scanner.py index 563b04b6d..20133a9a7 100644 --- a/infection_monkey/network/network_scanner.py +++ b/infection_monkey/network/network_scanner.py @@ -36,8 +36,32 @@ class NetworkScanner(object): self._ranges = [NetworkRange.get_range_obj(address_str=x) for x in WormConfiguration.subnet_scan_list] if WormConfiguration.local_network_scan: self._ranges += get_interfaces_ranges() + self._ranges += self._get_inaccessible_subnets_ips() LOG.info("Base local networks to scan are: %r", self._ranges) + def _get_inaccessible_subnets_ips(self): + """ + For each of the machine's IPs, checks if it's in one of the subnets specified in the + 'inaccessible_subnets' config value. If so, all other subnets in the config value shouldn't be accessible. + All these subnets are returned. + :return: A list of subnets that shouldn't be accessible from the machine the monkey is running on. + """ + subnets_to_scan = [] + if len(WormConfiguration.inaccessible_subnets) > 1: + for subnet_str in WormConfiguration.inaccessible_subnets: + if NetworkScanner._is_any_ip_in_subnet([unicode(x) for x in self._ip_addresses], subnet_str): + # If machine has IPs from 2 different subnets in the same group, there's no point checking the other + # subnet. + for other_subnet_str in WormConfiguration.inaccessible_subnets: + if other_subnet_str == subnet_str: + continue + if not NetworkScanner._is_any_ip_in_subnet([unicode(x) for x in self._ip_addresses], + other_subnet_str): + subnets_to_scan.append(NetworkRange.get_range_obj(other_subnet_str)) + break + + return subnets_to_scan + def get_victim_machines(self, scan_type, max_find=5, stop_callback=None): assert issubclass(scan_type, HostScanner) @@ -76,3 +100,10 @@ class NetworkScanner(object): if SCAN_DELAY: time.sleep(SCAN_DELAY) + + @staticmethod + def _is_any_ip_in_subnet(ip_addresses, subnet_str): + for ip_address in ip_addresses: + if NetworkRange.get_range_obj(subnet_str).is_in_range(ip_address): + return True + return False diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index c1133a9c8..74450cb29 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -6,6 +6,11 @@ import time import logging BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PARENT_PATH = os.path.dirname(BASE_PATH) + +if PARENT_PATH not in sys.path: + sys.path.insert(0, PARENT_PATH) + if BASE_PATH not in sys.path: sys.path.insert(0, BASE_PATH) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 7432fb638..c34db0eac 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -250,6 +250,31 @@ SCHEMA = { " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\"" } } + }, + "network_analysis": { + "title": "Network Analysis", + "type": "object", + "properties": { + "inaccessible_subnets": { + "title": "Network segmentation testing", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + ], + "description": + "Test for network segmentation by providing a list of" + " subnets that should NOT be accessible to each other." + " For example, given the following configuration:" + " '10.0.0.0/24, 11.0.0.2/32, 12.2.3.0/24'" + " a Monkey running on 10.0.0.5 will try to access machines in the following" + " subnets: 11.0.0.2/32, 12.2.3.0/24." + " An alert on successful connections will be shown in the report" + " Additional subnet formats include: 13.0.0.1, 13.0.0.1-13.0.0.5" + } + } } } }, diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index c3eaf4ed2..1d7542bc6 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,3 +1,6 @@ +import itertools +import functools + import ipaddress import logging from enum import Enum @@ -9,6 +12,7 @@ from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService from cc.utils import local_ip_addresses, get_subnets +from common.network.network_range import NetworkRange __author__ = "itay.mizeretz" @@ -358,7 +362,7 @@ class ReportService: ] @staticmethod - def get_cross_segment_issues(): + def get_island_cross_segment_issues(): issues = [] island_ips = local_ip_addresses() for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}): @@ -373,17 +377,160 @@ class ReportService: break if not found_good_ip: issues.append( - {'type': 'cross_segment', 'machine': monkey['hostname'], + {'type': 'island_cross_segment', 'machine': monkey['hostname'], 'networks': [str(subnet) for subnet in monkey_subnets], 'server_networks': [str(subnet) for subnet in get_subnets()]} ) return issues + @staticmethod + def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): + """ + Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. + :param ip_addresses: List of IP addresses to test. + :param source_subnet: Subnet to want an IP to not be in. + :param target_subnet: Subnet we want an IP to be in. + :return: + """ + for ip_address in ip_addresses: + if target_subnet.is_in_range(ip_address): + return None + for ip_address in ip_addresses: + if source_subnet.is_in_range(ip_address): + return ip_address + return None + + @staticmethod + def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range): + """ + Gets list of cross segment issues of a single machine. Meaning a machine has an interface for each of the + subnets. + :param source_subnet_range: The subnet range which shouldn't be able to access target_subnet. + :param target_subnet_range: The subnet range which shouldn't be accessible from source_subnet. + :return: + """ + cross_segment_issues = [] + + for monkey in mongo.db.monkey.find({}, {'ip_addresses': 1, 'hostname': 1}): + ip_in_src = None + ip_in_dst = None + for ip_addr in monkey['ip_addresses']: + if source_subnet_range.is_in_range(unicode(ip_addr)): + ip_in_src = ip_addr + break + + # No point searching the dst subnet if there are no IPs in src subnet. + if not ip_in_src: + continue + + for ip_addr in monkey['ip_addresses']: + if target_subnet_range.is_in_range(unicode(ip_addr)): + ip_in_dst = ip_addr + break + + if ip_in_dst: + cross_segment_issues.append( + { + 'source': ip_in_src, + 'hostname': monkey['hostname'], + 'target': ip_in_dst, + 'services': None, + 'is_self': True + }) + + return cross_segment_issues + + @staticmethod + def get_cross_segment_issues_per_subnet_pair(scans, source_subnet, target_subnet): + """ + Gets list of cross segment issues from source_subnet to target_subnet. + :param scans: List of all scan telemetry entries. Must have monkey_guid, ip_addr and services. + This should be a PyMongo cursor object. + :param source_subnet: The subnet which shouldn't be able to access target_subnet. + :param target_subnet: The subnet which shouldn't be accessible from source_subnet. + :return: + """ + if source_subnet == target_subnet: + return [] + source_subnet_range = NetworkRange.get_range_obj(source_subnet) + target_subnet_range = NetworkRange.get_range_obj(target_subnet) + + cross_segment_issues = [] + + scans.rewind() # If we iterated over scans already we need to rewind. + for scan in scans: + target_ip = scan['data']['machine']['ip_addr'] + if target_subnet_range.is_in_range(unicode(target_ip)): + monkey = NodeService.get_monkey_by_guid(scan['monkey_guid']) + cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], + source_subnet_range, + target_subnet_range) + + if cross_segment_ip is not None: + cross_segment_issues.append( + { + 'source': cross_segment_ip, + 'hostname': monkey['hostname'], + 'target': target_ip, + 'services': scan['data']['machine']['services'], + 'is_self': False + }) + + return cross_segment_issues + ReportService.get_cross_segment_issues_of_single_machine( + source_subnet_range, target_subnet_range) + + @staticmethod + def get_cross_segment_issues_per_subnet_group(scans, subnet_group): + """ + Gets list of cross segment issues within given subnet_group. + :param scans: List of all scan telemetry entries. Must have monkey_guid, ip_addr and services. + This should be a PyMongo cursor object. + :param subnet_group: List of subnets which shouldn't be accessible from each other. + :return: Cross segment issues regarding the subnets in the group. + """ + cross_segment_issues = [] + + for subnet_pair in itertools.product(subnet_group, subnet_group): + source_subnet = subnet_pair[0] + target_subnet = subnet_pair[1] + pair_issues = ReportService.get_cross_segment_issues_per_subnet_pair(scans, source_subnet, target_subnet) + if len(pair_issues) != 0: + cross_segment_issues.append( + { + 'source_subnet': source_subnet, + 'target_subnet': target_subnet, + 'issues': pair_issues + }) + + return cross_segment_issues + + @staticmethod + def get_cross_segment_issues(): + scans = mongo.db.telemetry.find({'telem_type': 'scan'}, + {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1}) + + cross_segment_issues = [] + + # For now the feature is limited to 1 group. + subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] + + for subnet_group in subnet_groups: + cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group) + + return cross_segment_issues + @staticmethod def get_issues(): - issues = ReportService.get_exploits() + ReportService.get_tunnels() +\ - ReportService.get_cross_segment_issues() + ReportService.get_azure_issues() + ISSUE_GENERATORS = [ + ReportService.get_exploits, + ReportService.get_tunnels, + ReportService.get_island_cross_segment_issues, + ReportService.get_azure_issues + ] + + issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) + issues_dict = {} for issue in issues: machine = issue['machine'] @@ -461,16 +608,19 @@ class ReportService: return issues_byte_array @staticmethod - def get_warnings_overview(issues): + def get_warnings_overview(issues, cross_segment_issues): warnings_byte_array = [False] * 2 for machine in issues: for issue in issues[machine]: - if issue['type'] == 'cross_segment': + if issue['type'] == 'island_cross_segment': warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True elif issue['type'] == 'tunnel': warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True + if len(cross_segment_issues) != 0: + warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True + return warnings_byte_array @staticmethod @@ -493,6 +643,7 @@ class ReportService: issues = ReportService.get_issues() config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() + cross_segment_issues = ReportService.get_cross_segment_issues() report = \ { @@ -507,7 +658,8 @@ class ReportService: 'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"), 'monkey_duration': ReportService.get_monkey_duration(), 'issues': ReportService.get_issues_overview(issues, config_users, config_passwords), - 'warnings': ReportService.get_warnings_overview(issues) + 'warnings': ReportService.get_warnings_overview(issues, cross_segment_issues), + 'cross_segment_issues': cross_segment_issues }, 'glance': { diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 791582e2c..c592464f0 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -367,6 +367,21 @@ class ReportPageComponent extends AuthComponent { } + { this.state.report.overview.cross_segment_issues.length > 0 ? +