From 7179d840a76e8042f51320b543528ad3e3d994c2 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 19 Nov 2018 15:40:16 +0200 Subject: [PATCH 01/11] adding the exporter father class and aws implement --- monkey/monkey_island/cc/resources/aws_exporter.py | 9 +++++++++ monkey/monkey_island/cc/resources/exporter.py | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 monkey/monkey_island/cc/resources/aws_exporter.py create mode 100644 monkey/monkey_island/cc/resources/exporter.py diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py new file mode 100644 index 000000000..cca47d968 --- /dev/null +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -0,0 +1,9 @@ +from exporter import Exporter + +class AWSExporter(Exporter): + + def __init__(self): + Exporter.__init__(self) + + def handle_report(self, report_json): + pass \ No newline at end of file diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py new file mode 100644 index 000000000..98f3e7662 --- /dev/null +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -0,0 +1,9 @@ + + +class Exporter: + + def __init__(self): + pass + + def handle_report(self, report_json): + raise NotImplementedError From 271c024574b83fc45418329b85ef03faef629b0c Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 12:39:47 +0200 Subject: [PATCH 02/11] * Added env' config * Added exporters and aws exporter * changed report generation to be automatic on monkey death with support of on-demand report generation and mongo storage --- .../cc/environment/environment.py | 2 + .../cc/resources/aws_exporter.py | 294 +++++++++++++++++- monkey/monkey_island/cc/resources/exporter.py | 4 +- monkey/monkey_island/cc/resources/root.py | 2 + monkey/monkey_island/cc/services/node.py | 4 + monkey/monkey_island/cc/services/report.py | 62 +++- monkey/monkey_island/requirements.txt | 1 + 7 files changed, 345 insertions(+), 24 deletions(-) diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 9e89208ef..70fc025c3 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -5,6 +5,8 @@ import aws logger = logging.getLogger(__name__) +AWS = 'aws' +STANDARD = 'standard' ENV_DICT = { 'standard': standard.StandardEnvironment, diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index cca47d968..363114948 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -1,9 +1,293 @@ -from exporter import Exporter +import logging +import uuid +from datetime import datetime +import boto3 + +from cc.resources.exporter import Exporter + +logger = logging.getLogger(__name__) + class AWSExporter(Exporter): - def __init__(self): - Exporter.__init__(self) + @staticmethod + def handle_report(report_json): - def handle_report(self, report_json): - pass \ No newline at end of file + findings_list = [] + issues_list = report_json['recommendations']['issues'] + for machine in issues_list: + for issue in issues_list[machine]: + findings_list.append(AWSExporter._prepare_finding(issue)) + + if not AWSExporter._send_findings(findings_list): + logger.error('Exporting findings to aws failed') + return False + + return True + + @staticmethod + def merge_two_dicts(x, y): + z = x.copy() # start with x's keys and values + z.update(y) # modifies z with y's keys and values & returns None + return z + + @staticmethod + def _prepare_finding(issue): + findings_dict = { + 'island_cross_segment': AWSExporter._handle_island_cross_segment_issue, + 'ssh': AWSExporter._handle_ssh_issue, + 'shellshock': AWSExporter._handle_shellshock_issue, + 'tunnel': AWSExporter._handle_tunnel_issue, + 'elastic': AWSExporter._handle_elastic_issue, + 'smb_password': AWSExporter._handle_smb_password_issue, + 'smb_pth': AWSExporter._handle_smb_pth_issue, + 'sambacry': AWSExporter._handle_sambacry_issue, + 'shared_passwords': AWSExporter._handle_shared_passwords_issue, + } + + finding = { + "SchemaVersion": "2018-10-08", + "Id": uuid.uuid4().hex, + "ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty", + "GeneratorId": issue['type'], + "AwsAccountId": "324264561773", + "Types": [ + "Software and Configuration Checks/Vulnerabilities/CVE" + ], + "CreatedAt": datetime.now().isoformat() + 'Z', + "UpdatedAt": datetime.now().isoformat() + 'Z', + } + return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) + + @staticmethod + def _send_findings(findings_list): + + securityhub = boto3.client('securityhub') + import_response = securityhub.batch_import_findings(Findings=findings_list) + print import_response + if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + return True + else: + return False + + @staticmethod + def _handle_tunnel_issue(issue): + finding =\ + { + "Severity": { + "Product": 5, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['dest'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Weak segmentation - Machines were able to communicate over unused ports." + finding["Description"] = "Use micro-segmentation policies to disable communication other than the required." + finding["Remediation"] = { + "Recommendation": { + "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" + .format(issue['machine'], issue['dest']) + } + } + return finding + + @staticmethod + def _handle_sambacry_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": str(issue['ip_address']) + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Samba servers are vulnerable to 'SambaCry'" + finding["Description"] = "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up."\ + .format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(issue['machine'], issue['ip_address'], issue['username']) + } + } + return finding + + @staticmethod + def _handle_smb_pth_issue(issue): + finding = \ + { + "Severity": { + "Product": 5, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format(issue['machine'], issue['ip_address'], issue['username']) + } + } + return finding + + @staticmethod + def _handle_ssh_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format(issue['machine'], issue['ip_address'], issue['username']) + } + } + return finding + + @staticmethod + def _handle_elastic_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Elasticsearch servers are vulnerable to CVE-2015-1427" + finding["Description"] = "Update your Elastic Search server to version 1.4.3 and up." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(issue['machine'], issue['ip_address']) + } + } + return finding + + @staticmethod + def _handle_island_cross_segment_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['networks'][0][:-2] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Weak segmentation - Machines from different segments are able to communicate." + finding["Description"] = "egment your network and make sure there is no communication between machines from different segments." + finding["Remediation"] = { + "Recommendation": { + "Text": "The network can probably be segmented. A monkey instance on \ + {0} in the networks {1} \ + could directly access the Monkey Island server in the networks {2}.".format(issue['machine'], + issue['networks'], + issue['server_networks']) + } + } + return finding + + @staticmethod + def _handle_shared_passwords_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": '10.0.0.1' + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Multiple users have the same password" + finding["Description"] = "Some users are sharing passwords, this should be fixed by changing passwords." + finding["Remediation"] = { + "Recommendation": { + "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) + } + } + return finding + + @staticmethod + def _handle_shellshock_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are vulnerable to 'Shellshock'" + finding["Description"] = "Update your Bash to a ShellShock-patched version." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(issue['machine'], issue['ip_address'], issue['port'], issue['paths']) + } + } + return finding + + @staticmethod + def _handle_smb_password_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format(issue['machine'], issue['ip_address'], issue['username']) + } + } + return finding diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py index 98f3e7662..1cf0c1b10 100644 --- a/monkey/monkey_island/cc/resources/exporter.py +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -1,9 +1,9 @@ - class Exporter: def __init__(self): pass - def handle_report(self, report_json): + @staticmethod + def handle_report(report_json): raise NotImplementedError diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 1d9141589..10e8f5170 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -65,5 +65,7 @@ class Root(flask_restful.Resource): if not infection_done: report_done = False else: + if is_any_exists: + ReportService.get_report() report_done = ReportService.is_report_generated() return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 072917974..1f9b68ebe 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -294,6 +294,10 @@ class NodeService: def is_monkey_finished_running(): return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive() + @staticmethod + def get_latest_modified_monkey(): + return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1) + @staticmethod def add_credentials_to_monkey(monkey_id, creds): mongo.db.monkey.update( diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index d8f9b9b96..b9fdf89e7 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -8,6 +8,8 @@ from enum import Enum from six import text_type from cc.database import mongo +from cc.environment.environment import load_env_from_file, AWS +from cc.resources.aws_exporter import AWSExporter from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService @@ -123,9 +125,9 @@ class ReportService: 'label': node['label'], 'ip_addresses': node['ip_addresses'], 'accessible_from_nodes': - (x['hostname'] for x in + list((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))), + for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))), 'services': node['services'] }) @@ -659,26 +661,19 @@ class ReportService: @staticmethod def is_report_generated(): - generated_report = mongo.db.report.find_one({'name': 'generated_report'}) + generated_report = mongo.db.report.find_one({}) if generated_report is None: return False - return generated_report['value'] + return True @staticmethod - def set_report_generated(): - mongo.db.report.update( - {'name': 'generated_report'}, - {'$set': {'value': True}}, - upsert=True) - logger.info("Report marked as generated.") - - @staticmethod - def get_report(): + def generate_report(): domain_issues = ReportService.get_domain_issues() issues = ReportService.get_issues() config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() cross_segment_issues = ReportService.get_cross_segment_issues() + monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime'] report = \ { @@ -710,17 +705,50 @@ class ReportService: { 'issues': issues, 'domain_issues': domain_issues + }, + 'meta': + { + 'latest_monkey_modifytime': monkey_latest_modify_time } } - - finished_run = NodeService.is_monkey_finished_running() - if finished_run: - ReportService.set_report_generated() + ReportService.export_to_exporters(report) + mongo.db.report.drop() + mongo.db.report.insert_one(report) return report + @staticmethod + def is_latest_report_exists(): + latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1}) + + if latest_report_doc: + report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime'] + latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime'] + return report_latest_modifytime == latest_monkey_modifytime + + return False + + @staticmethod + def get_report(): + if ReportService.is_latest_report_exists(): + return mongo.db.report.find_one() + return ReportService.generate_report() + @staticmethod def did_exploit_type_succeed(exploit_type): return mongo.db.edge.count( {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, limit=1) > 0 + + @staticmethod + def get_active_exporters(): + # This function should be in another module in charge of building a list of active exporters + exporters_list = [] + if load_env_from_file() == AWS: + exporters_list.append(AWSExporter) + return exporters_list + + @staticmethod + def export_to_exporters(report): + for exporter in ReportService.get_active_exporters(): + exporter.handle_report(report) diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 29c364c9f..f094df947 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -14,3 +14,4 @@ netifaces ipaddress enum34 PyCrypto +boto3 \ No newline at end of file From d21558e81a978f8e00d37873039967ebc36a6948 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 14:17:20 +0200 Subject: [PATCH 03/11] * encrypted config --- monkey/monkey_island/cc/services/config.py | 37 ++++++++++++++++++- .../ui/src/components/pages/ConfigurePage.js | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 64b359f61..33223a6e7 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -862,7 +862,37 @@ SCHEMA = { } } } - } + }, + 'island_configuration': { + 'title': 'Island Configuration', + 'type': 'object', + 'properties': + { + 'aws_config': + { + 'title': 'AWS Configuration', + 'type': 'object', + 'properties': + { + 'iam_role_id': + { + 'title': 'IAM role ID', + 'type': 'string' + }, + 'aws_access_key': + { + 'title': 'AWS access key ID', + 'type': 'string' + }, + 'aws_secret_access_key': + { + 'title': 'AWS Secret Access Key', + 'type': 'string' + } + } + } + } + } }, "options": { "collapsed": True @@ -874,7 +904,10 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['basic', 'credentials', 'exploit_password_list'], ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'] + ['internal', 'exploits', 'exploit_ssh_keys'], + ['island_configuration', 'aws_config', 'iam_role_id'], + ['island_configuration', 'aws_config', 'aws_access_key'], + ['island_configuration', 'aws_config', 'aws_secret_access_key'], ] 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 a97447df0..7e08170e2 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -10,7 +10,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentSection = 'basic'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; + this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal', 'monkey_island']; // set schema from server this.state = { From 2dfbc1645082feff66ea56d76fab6bcfd536b27b Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 11:48:43 +0200 Subject: [PATCH 04/11] * Added aws creds keys to configuration * Added boto session creation using credentials * Added a flag in the get_config function to separate island configuration values from monkey ones. * --- .../cc/resources/aws_exporter.py | 21 ++++++- monkey/monkey_island/cc/services/config.py | 63 +++++++++---------- .../ui/src/components/pages/ConfigurePage.js | 2 +- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 363114948..f8501c30c 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -4,9 +4,13 @@ from datetime import datetime import boto3 from cc.resources.exporter import Exporter +from cc.services.config import ConfigService logger = logging.getLogger(__name__) +AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_secret_access_key']] + class AWSExporter(Exporter): @@ -19,12 +23,21 @@ class AWSExporter(Exporter): for issue in issues_list[machine]: findings_list.append(AWSExporter._prepare_finding(issue)) - if not AWSExporter._send_findings(findings_list): + if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys()): logger.error('Exporting findings to aws failed') return False return True + @staticmethod + def _get_aws_keys(): + creds_dict = {} + for key in AWS_CRED_CONFIG_KEYS: + creds_dict[key[2]] = ConfigService.get_config_value(key) + + return creds_dict + + @staticmethod def merge_two_dicts(x, y): z = x.copy() # start with x's keys and values @@ -60,9 +73,11 @@ class AWSExporter(Exporter): return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) @staticmethod - def _send_findings(findings_list): + def _send_findings(findings_list, creds_dict): - securityhub = boto3.client('securityhub') + securityhub = boto3.client('securityhub', + aws_access_key_id=creds_dict.get('aws_access_key_id', ''), + aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) import_response = securityhub.batch_import_findings(Findings=findings_list) print import_response if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 33223a6e7..b5ef28f65 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -639,6 +639,28 @@ SCHEMA = { "description": "The current command server the monkey is communicating with" } } + }, + 'aws_config': { + 'title': 'AWS Configuration', + 'type': 'object', + 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', + 'properties': { + 'iam_role_id': { + 'title': 'IAM role ID', + 'type': 'string', + 'description': '' + }, + 'aws_access_key_id': { + 'title': 'AWS access key ID', + 'type': 'string', + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.' + }, + 'aws_secret_access_key': { + 'title': 'AWS secret access key', + 'type': 'string', + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.' + } + } } } }, @@ -863,36 +885,6 @@ SCHEMA = { } } }, - 'island_configuration': { - 'title': 'Island Configuration', - 'type': 'object', - 'properties': - { - 'aws_config': - { - 'title': 'AWS Configuration', - 'type': 'object', - 'properties': - { - 'iam_role_id': - { - 'title': 'IAM role ID', - 'type': 'string' - }, - 'aws_access_key': - { - 'title': 'AWS access key ID', - 'type': 'string' - }, - 'aws_secret_access_key': - { - 'title': 'AWS Secret Access Key', - 'type': 'string' - } - } - } - } - } }, "options": { "collapsed": True @@ -905,9 +897,9 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], ['internal', 'exploits', 'exploit_ssh_keys'], - ['island_configuration', 'aws_config', 'iam_role_id'], - ['island_configuration', 'aws_config', 'aws_access_key'], - ['island_configuration', 'aws_config', 'aws_secret_access_key'], + # ['cnc', 'aws_config', 'iam_role_id'], + # ['cnc', 'aws_config', 'aws_access_key_id'], + # ['cnc', 'aws_config', 'aws_secret_access_key'], ] @@ -918,11 +910,12 @@ class ConfigService: pass @staticmethod - def get_config(is_initial_config=False, should_decrypt=True): + def get_config(is_initial_config=False, should_decrypt=True, is_island=False): """ Gets the entire global config. :param is_initial_config: If True, the initial config will be returned instead of the current config. :param should_decrypt: If True, all config values which are set as encrypted will be decrypted. + :param is_island: If True, will include island specific configuration parameters. :return: The entire global config. """ config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {} @@ -930,6 +923,8 @@ class ConfigService: config.pop(field, None) if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) + if not is_island: + config['cnc'].pop('aws_config', None) return config @staticmethod 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 7e08170e2..a97447df0 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -10,7 +10,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentSection = 'basic'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal', 'monkey_island']; + this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; // set schema from server this.state = { From 30a6d7542fc26e1f7eda497c5803b2f07142ed78 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 12:12:24 +0200 Subject: [PATCH 05/11] * deleted a line --- monkey/monkey_island/cc/resources/aws_exporter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index f8501c30c..6295f28f3 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -37,7 +37,6 @@ class AWSExporter(Exporter): return creds_dict - @staticmethod def merge_two_dicts(x, y): z = x.copy() # start with x's keys and values From a79c60e9bc2344c8cf4034abca733f2d25af98eb Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 12:59:06 +0200 Subject: [PATCH 06/11] * added instance ID to each issue in an aws machine * changed findings resource to ec2 instance id instead of IP --- .../cc/resources/aws_exporter.py | 36 +++++++++---------- .../monkey_island/cc/resources/telemetry.py | 2 ++ monkey/monkey_island/cc/services/report.py | 7 ++++ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 6295f28f3..3f138e688 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -93,8 +93,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['dest'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -118,8 +118,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": str(issue['ip_address']) + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -143,8 +143,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -167,8 +167,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -191,8 +191,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -215,8 +215,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['networks'][0][:-2] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -243,8 +243,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": '10.0.0.1' + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -267,8 +267,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -291,8 +291,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 0db3b0eb4..6fc8f06f8 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -191,6 +191,8 @@ class Telemetry(flask_restful.Resource): if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() + if 'aws' in telemetry_json['data']: + mongo.db.monkey.insert({'aws_instance_id': telemetry_json['data']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index b9fdf89e7..7f4864e60 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -548,6 +548,10 @@ class ReportService: logger.info('Domain issues generated for reporting') return domain_issues_dict + @staticmethod + def get_machine_aws_instance_id(hostname): + return str(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + @staticmethod def get_issues(): ISSUE_GENERATORS = [ @@ -564,8 +568,11 @@ class ReportService: for issue in issues: if issue.get('is_local', True): machine = issue.get('machine').upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) if machine not in issues_dict: issues_dict[machine] = [] + if aws_instance_id: + issue['aws_instance_id'] = aws_instance_id issues_dict[machine].append(issue) logger.info('Issues generated for reporting') return issues_dict From 4cc85448d7d8c7769b9a4ae4b3dab14335b04ef2 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 14:01:46 +0200 Subject: [PATCH 07/11] * add instance id to domain issues too --- monkey/monkey_island/cc/services/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 7f4864e60..a75fdb7dd 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -542,8 +542,11 @@ class ReportService: for issue in issues: if not issue.get('is_local', True): machine = issue.get('machine').upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) if machine not in domain_issues_dict: domain_issues_dict[machine] = [] + if aws_instance_id: + issue['aws_instance_id'] = aws_instance_id domain_issues_dict[machine].append(issue) logger.info('Domain issues generated for reporting') return domain_issues_dict From 984a64561e305e3c27aea9d5a801371f500647ea Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 15:04:25 +0200 Subject: [PATCH 08/11] * a small fixup --- monkey/monkey_island/cc/resources/telemetry.py | 2 +- monkey/monkey_island/cc/services/config.py | 6 +++--- monkey/monkey_island/cc/services/report.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 6fc8f06f8..ab911a119 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -192,7 +192,7 @@ class Telemetry(flask_restful.Resource): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() if 'aws' in telemetry_json['data']: - mongo.db.monkey.insert({'aws_instance_id': telemetry_json['data']['instance-id']}) + mongo.db.monkey.update_one({'_id': monkey_id}, {'aws_instance_id': telemetry_json['data']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index b5ef28f65..52bafa36f 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -645,10 +645,10 @@ SCHEMA = { 'type': 'object', 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', 'properties': { - 'iam_role_id': { - 'title': 'IAM role ID', + 'aws_account_id': { + 'title': 'AWS account ID', 'type': 'string', - 'description': '' + 'description': 'Your AWS account ID that is subscribed to security hub feeds' }, 'aws_access_key_id': { 'title': 'AWS access key ID', diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index a75fdb7dd..a002235a0 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -553,7 +553,7 @@ class ReportService: @staticmethod def get_machine_aws_instance_id(hostname): - return str(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + return str(list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))[0]['aws_instance_id']) @staticmethod def get_issues(): @@ -754,7 +754,7 @@ class ReportService: def get_active_exporters(): # This function should be in another module in charge of building a list of active exporters exporters_list = [] - if load_env_from_file() == AWS: + if str(load_env_from_file()) == 'standard': exporters_list.append(AWSExporter) return exporters_list From 8eca2ca1e91e60ab2c955342848862e82717b11a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 10:28:41 +0200 Subject: [PATCH 09/11] * Exceptions handling for sending findings --- monkey/monkey_island/cc/resources/aws_exporter.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 3f138e688..c2082629c 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -77,11 +77,15 @@ class AWSExporter(Exporter): securityhub = boto3.client('securityhub', aws_access_key_id=creds_dict.get('aws_access_key_id', ''), aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) - import_response = securityhub.batch_import_findings(Findings=findings_list) - print import_response - if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: - return True - else: + try: + import_response = securityhub.batch_import_findings(Findings=findings_list) + print import_response + if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + return True + else: + return False + except Exception as e: + logger.error('AWS security hub findings failed to send.') return False @staticmethod From c47572cd532bcd55cc5b4b111c5a13882f174b18 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 11:08:43 +0200 Subject: [PATCH 10/11] * Added another configuration endpoint for the island specific fields --- monkey/monkey_island/cc/app.py | 2 ++ .../cc/resources/island_configuration.py | 24 +++++++++++++++++++ monkey/monkey_island/cc/services/config.py | 19 ++++++++------- .../ui/src/components/pages/ConfigurePage.js | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/island_configuration.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a9682cc90..5bb94b611 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -18,6 +18,7 @@ from cc.resources.log import Log from cc.resources.island_logs import IslandLog from cc.resources.monkey import Monkey from cc.resources.monkey_configuration import MonkeyConfiguration +from cc.resources.island_configuration import IslandConfiguration from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap from cc.resources.node import Node @@ -104,6 +105,7 @@ def init_app(mongo_url): api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/') api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/') + api.add_resource(IslandConfiguration, '/api/configuration/island', '/api/configuration/island/') api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/', '/api/monkey/download/') api.add_resource(NetMap, '/api/netmap', '/api/netmap/') diff --git a/monkey/monkey_island/cc/resources/island_configuration.py b/monkey/monkey_island/cc/resources/island_configuration.py new file mode 100644 index 000000000..57fda34fe --- /dev/null +++ b/monkey/monkey_island/cc/resources/island_configuration.py @@ -0,0 +1,24 @@ +import json + +import flask_restful +from flask import request, jsonify, abort + +from cc.auth import jwt_required +from cc.services.config import ConfigService + + +class IslandConfiguration(flask_restful.Resource): + @jwt_required() + def get(self): + return jsonify(schema=ConfigService.get_config_schema(), + configuration=ConfigService.get_config(False, True, True)) + + @jwt_required() + def post(self): + config_json = json.loads(request.data) + if 'reset' in config_json: + ConfigService.reset_config() + else: + 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 1fb26cb1c..2058a61dd 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -648,17 +648,20 @@ SCHEMA = { 'aws_account_id': { 'title': 'AWS account ID', 'type': 'string', - 'description': 'Your AWS account ID that is subscribed to security hub feeds' + 'description': 'Your AWS account ID that is subscribed to security hub feeds', + 'default': " " }, 'aws_access_key_id': { 'title': 'AWS access key ID', 'type': 'string', - 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.' + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', + 'default': " " }, 'aws_secret_access_key': { 'title': 'AWS secret access key', 'type': 'string', - 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.' + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', + 'default': " " } } } @@ -897,16 +900,14 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['basic', 'credentials', 'exploit_password_list'], ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'], - # ['cnc', 'aws_config', 'iam_role_id'], - # ['cnc', 'aws_config', 'aws_access_key_id'], - # ['cnc', 'aws_config', 'aws_secret_access_key'], + ['internal', 'exploits', 'exploit_ssh_keys'] ] # This should be used for config values of string type ENCRYPTED_CONFIG_STRINGS = \ [ - + ['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_secret_access_key'] ] @@ -931,7 +932,7 @@ class ConfigService: if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) if not is_island: - config['cnc'].pop('aws_config', None) + config.get('cnc', {}).pop('aws_config', None) return config @staticmethod 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 a97447df0..6cc7e009a 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -24,7 +24,7 @@ class ConfigurePageComponent extends AuthComponent { } componentDidMount() { - this.authFetch('/api/configuration') + this.authFetch('/api/configuration/island') .then(res => res.json()) .then(res => { let sections = []; From 673605b72181b7cc2611cd72dd30012a394fcb18 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 14:13:50 +0200 Subject: [PATCH 11/11] * Added aws region getter * Moved productARN to server_config.json file --- monkey/monkey_island/cc/environment/aws.py | 4 ++++ monkey/monkey_island/cc/environment/environment.py | 9 ++++++--- monkey/monkey_island/cc/resources/aws_exporter.py | 7 +++++-- monkey/monkey_island/cc/server_config.json | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index b85a7d2e4..2a57f1cb7 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -15,6 +15,10 @@ class AwsEnvironment(Environment): def _get_instance_id(): return urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() + @staticmethod + def _get_region(): + return urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] + def is_auth_enabled(self): return True diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 70fc025c3..c15e70257 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -14,13 +14,16 @@ ENV_DICT = { } -def load_env_from_file(): +def load_server_configuration_from_file(): 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'] + return json.loads(config_content) +def load_env_from_file(): + config_json = load_server_configuration_from_file() + return config_json['server_config'] + try: __env_type = load_env_from_file() env = ENV_DICT[__env_type]() diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index c2082629c..480743026 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -5,6 +5,7 @@ import boto3 from cc.resources.exporter import Exporter from cc.services.config import ConfigService +from cc.environment.environment import load_server_configuration_from_file logger = logging.getLogger(__name__) @@ -57,10 +58,12 @@ class AWSExporter(Exporter): 'shared_passwords': AWSExporter._handle_shared_passwords_issue, } + product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') + finding = { "SchemaVersion": "2018-10-08", "Id": uuid.uuid4().hex, - "ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty", + "ProductArn": product_arn, "GeneratorId": issue['type'], "AwsAccountId": "324264561773", "Types": [ @@ -308,4 +311,4 @@ class AWSExporter(Exporter): "Text": "The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format(issue['machine'], issue['ip_address'], issue['username']) } } - return finding + return finding \ No newline at end of file diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 2d1a5995b..4d8644cbb 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,3 +1,6 @@ { - "server_config": "standard" + "server_config": "standard", + "aws": { + "sec_hub_product_arn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty" + } } \ No newline at end of file