diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py index 296179d41..04dff1441 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py @@ -3,25 +3,20 @@ import os import random import string import subprocess -import time -import win32event - -from infection_monkey.utils.windows.auto_new_user import AutoNewUser, NewUserError +from infection_monkey.utils.new_user_error import NewUserError +from infection_monkey.utils.auto_new_user_factory import create_auto_new_user from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER from infection_monkey.post_breach.pba import PBA from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.linux.users import get_linux_commands_to_delete_user, get_linux_commands_to_add_user PING_TEST_DOMAIN = "google.com" -PING_WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000 - CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged." CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})." -USERNAME = "somenewuser" +USERNAME_PREFIX = "somenewuser" PASSWORD = "N3WPa55W0rD!1" logger = logging.getLogger(__name__) @@ -38,94 +33,24 @@ class CommunicateAsNewUser(PBA): def run(self): username = CommunicateAsNewUser.get_random_new_user_name() - if is_windows_os(): - self.communicate_as_new_user_windows(username) - else: - self.communicate_as_new_user_linux(username) + try: + with create_auto_new_user(username, PASSWORD) as new_user: + ping_commandline = CommunicateAsNewUser.get_commandline_for_ping() + exit_status = new_user.run_as(ping_commandline) + self.send_ping_result_telemetry(exit_status, ping_commandline, username) + except subprocess.CalledProcessError as e: + PostBreachTelem(self, (e.output, False)).send() + except NewUserError as e: + PostBreachTelem(self, (str(e), False)).send() @staticmethod def get_random_new_user_name(): - return USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) + return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) - def communicate_as_new_user_linux(self, username): - try: - # add user + ping - linux_cmds = get_linux_commands_to_add_user(username) - commandline = "ping -c 1 {}".format(PING_TEST_DOMAIN) - linux_cmds.extend([";", "sudo", "-u", username, commandline]) - final_command = ' '.join(linux_cmds) - exit_status = os.system(final_command) - self.send_ping_result_telemetry(exit_status, commandline, username) - # delete the user, async in case it gets stuck. - _ = subprocess.Popen( - get_linux_commands_to_delete_user(username), stderr=subprocess.STDOUT, shell=True) - # Leaking the process on purpose - nothing we can do if it's stuck. - except subprocess.CalledProcessError as e: - PostBreachTelem(self, (e.output, False)).send() - - def communicate_as_new_user_windows(self, username): - # Importing these only on windows, as they won't exist on linux. - import win32con - import win32process - import win32api - - try: - with AutoNewUser(username, PASSWORD) as new_user: - # Using os.path is OK, as this is on windows for sure - ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe") - if not os.path.exists(ping_app_path): - PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send() - return # Can't continue without ping. - - try: - # Open process as that user: - # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera - commandline = "{} {} {} {}".format(ping_app_path, PING_TEST_DOMAIN, "-n", "1") - process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser( - new_user.get_logon_handle(), # A handle to the primary token that represents a user. - None, # The name of the module to be executed. - commandline, # The command line to be executed. - None, # Process attributes - None, # Thread attributes - True, # Should inherit handles - win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process. - None, # An environment block for the new process. If this parameter is NULL, the new process - # uses the environment of the calling process. - None, # CWD. If this parameter is NULL, the new process will have the same current drive and - # directory as the calling process. - win32process.STARTUPINFO() # STARTUPINFO structure. - # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa - ) - - logger.debug( - "Waiting for ping process to finish. Timeout: {}ms".format(PING_WAIT_TIMEOUT_IN_MILLISECONDS)) - - # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later. - _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out. - process_handle, # Ping process handle - PING_WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds - ) - - ping_exit_code = win32process.GetExitCodeProcess(process_handle) - - self.send_ping_result_telemetry(ping_exit_code, commandline, username) - except Exception as e: - # If failed on 1314, it's possible to try to elevate the rights of the current user with the - # "Replace a process level token" right, using Local Security Policy editing. - PostBreachTelem(self, ( - "Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send() - finally: - try: - win32api.CloseHandle(process_handle) - win32api.CloseHandle(thread_handle) - except Exception as err: - logger.error("Close handle error: " + str(err)) - except subprocess.CalledProcessError as err: - PostBreachTelem(self, ( - "Couldn't create the user '{}'. Error output is: '{}'".format(username, str(err)), - False)).send() - except NewUserError as e: - PostBreachTelem(self, (str(e), False)).send() + @staticmethod + def get_commandline_for_ping(domain=PING_TEST_DOMAIN, is_windows=is_windows_os()): + format_string = "PING.exe {domain} -n 1" if is_windows else "ping -c 1 {domain}" + return format_string.format(domain=domain) def send_ping_result_telemetry(self, exit_status, commandline, username): """ diff --git a/monkey/infection_monkey/utils/auto_new_user.py b/monkey/infection_monkey/utils/auto_new_user.py new file mode 100644 index 000000000..e749020d6 --- /dev/null +++ b/monkey/infection_monkey/utils/auto_new_user.py @@ -0,0 +1,42 @@ +import logging +import abc + +logger = logging.getLogger(__name__) + + +class AutoNewUser: + """ + RAII object to use for creating and using a new user. Use with `with`. + User will be created when the instance is instantiated. + User will be available for use (log on for Windows, for example) at the start of the `with` scope. + User will be removed (deactivated and deleted for Windows, for example) at the end of said `with` scope. + + Example: + # Created # Logged on + with AutoNewUser("user", "pass", is_on_windows()) as new_user: + ... + ... + # Logged off and deleted + ... + """ + __metaclass__ = abc.ABCMeta + + def __init__(self, username, password): + self.username = username + self.password = password + + @abc.abstractmethod + def __enter__(self): + raise NotImplementedError() + + @abc.abstractmethod + def __exit__(self, exc_type, exc_val, exc_tb): + raise NotImplementedError() + + @abc.abstractmethod + def run_as(self, command): + """ + Run the given command as the new user that was created. + :param command: The command to run - give as shell commandline (e.g. "ping google.com -n 1") + """ + raise NotImplementedError() diff --git a/monkey/infection_monkey/utils/auto_new_user_factory.py b/monkey/infection_monkey/utils/auto_new_user_factory.py new file mode 100644 index 000000000..898226d46 --- /dev/null +++ b/monkey/infection_monkey/utils/auto_new_user_factory.py @@ -0,0 +1,21 @@ +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.linux.users import AutoNewLinuxUser +from infection_monkey.utils.windows.users import AutoNewWindowsUser + + +def create_auto_new_user(username, password, is_windows=is_windows_os()): + """ + Factory method for creating an AutoNewUser. See AutoNewUser's documentation for more information. + Example usage: + with create_auto_new_user(username, PASSWORD) as new_user: + ... + :param username: The username of the new user. + :param password: The password of the new user. + :param is_windows: If True, a new Windows user is created. Otherwise, a Linux user is created. Leave blank for + automatic detection. + :return: The new AutoNewUser object - use with a `with` scope. + """ + if is_windows: + return AutoNewWindowsUser(username, password) + else: + return AutoNewLinuxUser(username, password) diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py index 1acc87d72..34becb8f7 100644 --- a/monkey/infection_monkey/utils/linux/users.py +++ b/monkey/infection_monkey/utils/linux/users.py @@ -1,14 +1,21 @@ import datetime +import logging +import os +import subprocess + +from infection_monkey.utils.auto_new_user import AutoNewUser + +logger = logging.getLogger(__name__) def get_linux_commands_to_add_user(username): return [ - 'useradd', + 'useradd', # https://linux.die.net/man/8/useradd '-M', # Do not create homedir - '--expiredate', + '--expiredate', # The date on which the user account will be disabled. datetime.datetime.today().strftime('%Y-%m-%d'), - '--inactive', - '0', + '--inactive', # The number of days after a password expires until the account is permanently disabled. + '0', # A value of 0 disables the account as soon as the password has expired '-c', # Comment 'MONKEY_USER', # Comment username] @@ -19,3 +26,33 @@ def get_linux_commands_to_delete_user(username): 'deluser', username ] + + +class AutoNewLinuxUser(AutoNewUser): + """ + See AutoNewUser's documentation for details. + """ + + def __init__(self, username, password): + """ + Creates a user with the username + password. + :raises: subprocess.CalledProcessError if failed to add the user. + """ + super(AutoNewLinuxUser, self).__init__(username, password) + + commands_to_add_user = get_linux_commands_to_add_user(username) + logger.debug("Trying to add {} with commands {}".format(self.username, str(commands_to_add_user))) + _ = subprocess.check_output(' '.join(commands_to_add_user), stderr=subprocess.STDOUT, shell=True) + + def __enter__(self): + return self # No initialization/logging on needed in Linux + + def run_as(self, command): + command_as_new_user = "sudo -u {username} {command}".format(username=self.username, command=command) + return os.system(command_as_new_user) + + def __exit__(self, exc_type, exc_val, exc_tb): + # delete the user. + commands_to_delete_user = get_linux_commands_to_delete_user(self.username) + logger.debug("Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user))) + _ = subprocess.check_output(" ".join(commands_to_delete_user), stderr=subprocess.STDOUT, shell=True) diff --git a/monkey/infection_monkey/utils/new_user_error.py b/monkey/infection_monkey/utils/new_user_error.py new file mode 100644 index 000000000..8fe44d7bc --- /dev/null +++ b/monkey/infection_monkey/utils/new_user_error.py @@ -0,0 +1,2 @@ +class NewUserError(Exception): + pass diff --git a/monkey/infection_monkey/utils/windows/auto_new_user.py b/monkey/infection_monkey/utils/windows/auto_new_user.py deleted file mode 100644 index d95ac0bf0..000000000 --- a/monkey/infection_monkey/utils/windows/auto_new_user.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -import subprocess - -from infection_monkey.post_breach.actions.add_user import BackdoorUser -from infection_monkey.utils.windows.users import get_windows_commands_to_delete_user, get_windows_commands_to_add_user - -logger = logging.getLogger(__name__) - - -class NewUserError(Exception): - pass - - -class AutoNewUser(object): - """ - RAII object to use for creating and using a new user in Windows. Use with `with`. - User will be created when the instance is instantiated. - User will log on at the start of the `with` scope. - User will log off and get deleted at the end of said `with` scope. - - Example: - # Created # Logged on - with AutoNewUser("user", "pass") as new_user: - ... - ... - # Logged off and deleted - ... - """ - def __init__(self, username, password): - """ - Creates a user with the username + password. - :raises: subprocess.CalledProcessError if failed to add the user. - """ - self.username = username - self.password = password - - windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) - _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) - - def __enter__(self): - # Importing these only on windows, as they won't exist on linux. - import win32security - import win32con - - try: - # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera - self.logon_handle = win32security.LogonUser( - self.username, - ".", # Use current domain. - self.password, - win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). - win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. - except Exception as err: - raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err))) - return self - - def get_logon_handle(self): - return self.logon_handle - - def __exit__(self, exc_type, exc_val, exc_tb): - # Logoff - self.logon_handle.Close() - - # Try to delete user - try: - _ = subprocess.Popen( - get_windows_commands_to_delete_user(self.username), stderr=subprocess.STDOUT, shell=True) - except Exception as err: - raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err)) diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py index 0e6847cff..cf6eb73c4 100644 --- a/monkey/infection_monkey/utils/windows/users.py +++ b/monkey/infection_monkey/utils/windows/users.py @@ -1,3 +1,15 @@ +import logging +import subprocess + +from infection_monkey.utils.auto_new_user import AutoNewUser +from infection_monkey.utils.new_user_error import NewUserError + +ACTIVE_NO_NET_USER = '/ACTIVE:NO' +WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000 + +logger = logging.getLogger(__name__) + + def get_windows_commands_to_add_user(username, password, should_be_active=False): windows_cmds = [ 'net', @@ -6,7 +18,7 @@ def get_windows_commands_to_add_user(username, password, should_be_active=False) password, '/add'] if not should_be_active: - windows_cmds.append('/ACTIVE:NO') + windows_cmds.append(ACTIVE_NO_NET_USER) return windows_cmds @@ -16,3 +28,128 @@ def get_windows_commands_to_delete_user(username): 'user', username, '/delete'] + + +def get_windows_commands_to_deactivate_user(username): + return [ + 'net', + 'user', + username, + ACTIVE_NO_NET_USER] + + +class AutoNewWindowsUser(AutoNewUser): + """ + See AutoNewUser's documentation for details. + """ + + def __init__(self, username, password): + """ + Creates a user with the username + password. + :raises: subprocess.CalledProcessError if failed to add the user. + """ + super(AutoNewWindowsUser, self).__init__(username, password) + + windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) + logger.debug("Trying to add {} with commands {}".format(self.username, str(windows_cmds))) + _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) + + def __enter__(self): + # Importing these only on windows, as they won't exist on linux. + import win32security + import win32con + + try: + # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera + self.logon_handle = win32security.LogonUser( + self.username, + ".", # Use current domain. + self.password, + win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). Need this to open ping + # using a shell. + win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. + except Exception as err: + raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err))) + return self + + def run_as(self, command): + # Importing these only on windows, as they won't exist on linux. + import win32con + import win32process + import win32api + import win32event + + exit_code = -1 + process_handle = None + thread_handle = None + + try: + # Open process as that user: + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera + process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser( + self.get_logon_handle(), # A handle to the primary token that represents a user. + None, # The name of the module to be executed. + command, # The command line to be executed. + None, # Process attributes + None, # Thread attributes + True, # Should inherit handles + win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process. + None, # An environment block for the new process. If this parameter is NULL, the new process + # uses the environment of the calling process. + None, # CWD. If this parameter is NULL, the new process will have the same current drive and + # directory as the calling process. + win32process.STARTUPINFO() # STARTUPINFO structure. + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa + ) + + logger.debug( + "Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS)) + + # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later. + _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out. + process_handle, # Ping process handle + WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds + ) + + exit_code = win32process.GetExitCodeProcess(process_handle) + finally: + try: + if process_handle is not None: + win32api.CloseHandle(process_handle) + if thread_handle is not None: + win32api.CloseHandle(thread_handle) + except Exception as err: + logger.error("Close handle error: " + str(err)) + + return exit_code + + def get_logon_handle(self): + return self.logon_handle + + def __exit__(self, exc_type, exc_val, exc_tb): + # Logoff + self.logon_handle.Close() + + # Try to disable and then delete the user. + self.try_deactivate_user() + self.try_delete_user() + + def try_deactivate_user(self): + try: + commands_to_deactivate_user = get_windows_commands_to_deactivate_user(self.username) + logger.debug( + "Trying to deactivate {} with commands {}".format(self.username, str(commands_to_deactivate_user))) + _ = subprocess.check_output( + commands_to_deactivate_user, stderr=subprocess.STDOUT, shell=True) + except Exception as err: + raise NewUserError("Can't deactivate user {}. Info: {}".format(self.username, err)) + + def try_delete_user(self): + try: + commands_to_delete_user = get_windows_commands_to_delete_user(self.username) + logger.debug( + "Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user))) + _ = subprocess.check_output( + commands_to_delete_user, stderr=subprocess.STDOUT, shell=True) + except Exception as err: + raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err))