Merge pull request #120 from guardicore/feature/detect-cross-segment-traffic

Feature/detect cross segment traffic
This commit is contained in:
itaymmguardicore 2018-09-03 15:23:21 +03:00 committed by GitHub
commit 5ce902fecd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 261 additions and 10 deletions

View File

@ -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 = []

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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"
}
}
} }
} }
}, },

View File

@ -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':
{ {

View File

@ -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':