forked from p34709852/monkey
Merge pull request #120 from guardicore/feature/detect-cross-segment-traffic
Feature/detect cross segment traffic
This commit is contained in:
commit
5ce902fecd
|
@ -186,6 +186,7 @@ class Configuration(object):
|
||||||
local_network_scan = True
|
local_network_scan = True
|
||||||
|
|
||||||
subnet_scan_list = []
|
subnet_scan_list = []
|
||||||
|
inaccessible_subnets = []
|
||||||
|
|
||||||
blocked_ips = []
|
blocked_ips = []
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"subnet_scan_list": [
|
"subnet_scan_list": [
|
||||||
|
|
||||||
],
|
],
|
||||||
|
"inaccessible_subnets": [],
|
||||||
"blocked_ips": [],
|
"blocked_ips": [],
|
||||||
"current_server": "192.0.2.0:5000",
|
"current_server": "192.0.2.0:5000",
|
||||||
"alive": true,
|
"alive": true,
|
||||||
|
@ -96,4 +97,4 @@
|
||||||
"use_file_logging": true,
|
"use_file_logging": true,
|
||||||
"victims_max_exploit": 7,
|
"victims_max_exploit": 7,
|
||||||
"victims_max_find": 30
|
"victims_max_find": 30
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,32 @@ class NetworkScanner(object):
|
||||||
self._ranges = [NetworkRange.get_range_obj(address_str=x) for x in WormConfiguration.subnet_scan_list]
|
self._ranges = [NetworkRange.get_range_obj(address_str=x) for x in WormConfiguration.subnet_scan_list]
|
||||||
if WormConfiguration.local_network_scan:
|
if WormConfiguration.local_network_scan:
|
||||||
self._ranges += get_interfaces_ranges()
|
self._ranges += get_interfaces_ranges()
|
||||||
|
self._ranges += self._get_inaccessible_subnets_ips()
|
||||||
LOG.info("Base local networks to scan are: %r", self._ranges)
|
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):
|
def get_victim_machines(self, scan_type, max_find=5, stop_callback=None):
|
||||||
assert issubclass(scan_type, HostScanner)
|
assert issubclass(scan_type, HostScanner)
|
||||||
|
|
||||||
|
@ -76,3 +100,10 @@ class NetworkScanner(object):
|
||||||
|
|
||||||
if SCAN_DELAY:
|
if SCAN_DELAY:
|
||||||
time.sleep(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
|
||||||
|
|
|
@ -6,6 +6,11 @@ import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
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:
|
if BASE_PATH not in sys.path:
|
||||||
sys.path.insert(0, BASE_PATH)
|
sys.path.insert(0, BASE_PATH)
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,31 @@ SCHEMA = {
|
||||||
" Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\""
|
" 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import itertools
|
||||||
|
import functools
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
@ -9,6 +12,7 @@ from cc.services.config import ConfigService
|
||||||
from cc.services.edge import EdgeService
|
from cc.services.edge import EdgeService
|
||||||
from cc.services.node import NodeService
|
from cc.services.node import NodeService
|
||||||
from cc.utils import local_ip_addresses, get_subnets
|
from cc.utils import local_ip_addresses, get_subnets
|
||||||
|
from common.network.network_range import NetworkRange
|
||||||
|
|
||||||
__author__ = "itay.mizeretz"
|
__author__ = "itay.mizeretz"
|
||||||
|
|
||||||
|
@ -358,7 +362,7 @@ class ReportService:
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cross_segment_issues():
|
def get_island_cross_segment_issues():
|
||||||
issues = []
|
issues = []
|
||||||
island_ips = local_ip_addresses()
|
island_ips = local_ip_addresses()
|
||||||
for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}):
|
for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}):
|
||||||
|
@ -373,17 +377,160 @@ class ReportService:
|
||||||
break
|
break
|
||||||
if not found_good_ip:
|
if not found_good_ip:
|
||||||
issues.append(
|
issues.append(
|
||||||
{'type': 'cross_segment', 'machine': monkey['hostname'],
|
{'type': 'island_cross_segment', 'machine': monkey['hostname'],
|
||||||
'networks': [str(subnet) for subnet in monkey_subnets],
|
'networks': [str(subnet) for subnet in monkey_subnets],
|
||||||
'server_networks': [str(subnet) for subnet in get_subnets()]}
|
'server_networks': [str(subnet) for subnet in get_subnets()]}
|
||||||
)
|
)
|
||||||
|
|
||||||
return issues
|
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
|
@staticmethod
|
||||||
def get_issues():
|
def get_issues():
|
||||||
issues = ReportService.get_exploits() + ReportService.get_tunnels() +\
|
ISSUE_GENERATORS = [
|
||||||
ReportService.get_cross_segment_issues() + ReportService.get_azure_issues()
|
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 = {}
|
issues_dict = {}
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
machine = issue['machine']
|
machine = issue['machine']
|
||||||
|
@ -461,16 +608,19 @@ class ReportService:
|
||||||
return issues_byte_array
|
return issues_byte_array
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_warnings_overview(issues):
|
def get_warnings_overview(issues, cross_segment_issues):
|
||||||
warnings_byte_array = [False] * 2
|
warnings_byte_array = [False] * 2
|
||||||
|
|
||||||
for machine in issues:
|
for machine in issues:
|
||||||
for issue in issues[machine]:
|
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
|
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
|
||||||
elif issue['type'] == 'tunnel':
|
elif issue['type'] == 'tunnel':
|
||||||
warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True
|
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
|
return warnings_byte_array
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -493,6 +643,7 @@ class ReportService:
|
||||||
issues = ReportService.get_issues()
|
issues = ReportService.get_issues()
|
||||||
config_users = ReportService.get_config_users()
|
config_users = ReportService.get_config_users()
|
||||||
config_passwords = ReportService.get_config_passwords()
|
config_passwords = ReportService.get_config_passwords()
|
||||||
|
cross_segment_issues = ReportService.get_cross_segment_issues()
|
||||||
|
|
||||||
report = \
|
report = \
|
||||||
{
|
{
|
||||||
|
@ -507,7 +658,8 @@ class ReportService:
|
||||||
'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"),
|
'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"),
|
||||||
'monkey_duration': ReportService.get_monkey_duration(),
|
'monkey_duration': ReportService.get_monkey_duration(),
|
||||||
'issues': ReportService.get_issues_overview(issues, config_users, config_passwords),
|
'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':
|
'glance':
|
||||||
{
|
{
|
||||||
|
|
|
@ -367,6 +367,21 @@ class ReportPageComponent extends AuthComponent {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{ this.state.report.overview.cross_segment_issues.length > 0 ?
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
Segmentation Issues
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
The Monkey uncovered the following set of segmentation issues:
|
||||||
|
<ul>
|
||||||
|
{this.state.report.overview.cross_segment_issues.map(x => this.generateCrossSegmentIssue(x))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
''
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -450,6 +465,27 @@ class ReportPageComponent extends AuthComponent {
|
||||||
return data_array.map(badge_data => <span className="label label-info" style={{margin: '2px'}}>{badge_data}</span>);
|
return data_array.map(badge_data => <span className="label label-info" style={{margin: '2px'}}>{badge_data}</span>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateCrossSegmentIssue(crossSegmentIssue) {
|
||||||
|
return <li>
|
||||||
|
{'Communication possible from ' + crossSegmentIssue['source_subnet'] + ' to ' + crossSegmentIssue['target_subnet']}
|
||||||
|
<CollapsibleWellComponent>
|
||||||
|
<ul>
|
||||||
|
{crossSegmentIssue['issues'].map(x =>
|
||||||
|
x['is_self'] ?
|
||||||
|
<li>
|
||||||
|
{'Machine ' + x['hostname'] + ' has both ips: ' + x['source'] + ' and ' + x['target']}
|
||||||
|
</li>
|
||||||
|
:
|
||||||
|
<li>
|
||||||
|
{'IP ' + x['source'] + ' (' + x['hostname'] + ') connected to IP ' + x['target']
|
||||||
|
+ ' using the services: ' + Object.keys(x['services']).join(', ')}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</CollapsibleWellComponent>
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
|
||||||
generateShellshockPathListBadges(paths) {
|
generateShellshockPathListBadges(paths) {
|
||||||
return paths.map(path => <span className="label label-warning" style={{margin: '2px'}}>{path}</span>);
|
return paths.map(path => <span className="label label-warning" style={{margin: '2px'}}>{path}</span>);
|
||||||
}
|
}
|
||||||
|
@ -655,7 +691,7 @@ class ReportPageComponent extends AuthComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCrossSegmentIssue(issue) {
|
generateIslandCrossSegmentIssue(issue) {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
Segment your network and make sure there is no communication between machines from different segments.
|
Segment your network and make sure there is no communication between machines from different segments.
|
||||||
|
@ -773,7 +809,7 @@ class ReportPageComponent extends AuthComponent {
|
||||||
case 'conficker':
|
case 'conficker':
|
||||||
data = this.generateConfickerIssue(issue);
|
data = this.generateConfickerIssue(issue);
|
||||||
break;
|
break;
|
||||||
case 'cross_segment':
|
case 'island_cross_segment':
|
||||||
data = this.generateCrossSegmentIssue(issue);
|
data = this.generateCrossSegmentIssue(issue);
|
||||||
break;
|
break;
|
||||||
case 'tunnel':
|
case 'tunnel':
|
||||||
|
|
Loading…
Reference in New Issue