From e618378c955140521e634d4024a9b9634636a0ab Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Thu, 5 Sep 2019 20:56:48 +0300 Subject: [PATCH] Vastly improved communicate as new user PBA code structure, also not leaking any more process or thread handles. --- .../infection_monkey/monkey_utils/__init__.py | 0 .../monkey_utils/windows/__init__.py | 0 .../monkey_utils/windows/new_user.py | 64 ++++++++ .../actions/communicate_as_new_user.py | 151 ++++++++---------- 4 files changed, 134 insertions(+), 81 deletions(-) create mode 100644 monkey/infection_monkey/monkey_utils/__init__.py create mode 100644 monkey/infection_monkey/monkey_utils/windows/__init__.py create mode 100644 monkey/infection_monkey/monkey_utils/windows/new_user.py diff --git a/monkey/infection_monkey/monkey_utils/__init__.py b/monkey/infection_monkey/monkey_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/monkey_utils/windows/__init__.py b/monkey/infection_monkey/monkey_utils/windows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/monkey_utils/windows/new_user.py b/monkey/infection_monkey/monkey_utils/windows/new_user.py new file mode 100644 index 000000000..be6e2534d --- /dev/null +++ b/monkey/infection_monkey/monkey_utils/windows/new_user.py @@ -0,0 +1,64 @@ +import logging +import subprocess + +from infection_monkey.post_breach.actions.add_user import BackdoorUser +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem + + +logger = logging.getLogger(__name__) + + +class NewUserError(Exception): + pass + + +class NewUser(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 start of `with` scope. + User will log off on end of `with` scope. + + Example: + # Created # Logged on + with NewUser("user", "pass") as new_user: + ... + ... + # Logged off + ... + """ + 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 = BackdoorUser.get_windows_commands_to_add_user(self.username, self.password, True) + logger.debug("Trying these commands: {}".format(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). + 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): + self.logon_handle.Close() + # TODO Delete user 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 182acea00..8869a225f 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 @@ -5,6 +5,7 @@ import string import subprocess from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +from infection_monkey.monkey_utils.windows.new_user import NewUser, NewUserError from infection_monkey.post_breach.actions.add_user import BackdoorUser from infection_monkey.post_breach.pba import PBA from infection_monkey.telemetry.post_breach_telem import PostBreachTelem @@ -31,88 +32,76 @@ class CommunicateAsNewUser(PBA): def run(self): username = USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) if is_windows_os(): - # Importing these only on windows, as they won't exist on linux. - import win32con - import win32process - import win32security - - if not self.try_to_create_user_windows(username, PASSWORD): - return # no point to continue if failed creating the user. - - try: - # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera - new_user_logon_token_handle = win32security.LogonUser( - username, - ".", # use current domain - PASSWORD, - win32con.LOGON32_LOGON_INTERACTIVE, # logon type - interactive (normal user) - win32con.LOGON32_PROVIDER_DEFAULT) # logon provider - except Exception as e: - PostBreachTelem( - self, - ("Can't logon as {}. Error: {}".format(username, e.message), False) - ).send() - return # no point to continue if can't log on. - - # 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, "google.com") - _ = win32process.CreateProcessAsUser( - new_user_logon_token_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 - ) - - PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_WINDOWS_FORMAT.format(commandline, username), True)).send() - return - except Exception as e: - # TODO: if failed on 1314, we can try to add elevate the rights of the current user with the "Replace a - # process level token" right, using Local Security Policy editing. Worked, but only after reboot. So: - # 1. need to decide if worth it, and then - # 2. need to find how to do this using python... - PostBreachTelem(self, ( - "Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send() - return + self.communicate_as_new_user_windows(username) else: - try: - linux_cmds = BackdoorUser.get_linux_commands_to_add_user(username) - commandline = "ping -c 2 google.com" - linux_cmds.extend([";", "sudo", "-u", username, commandline]) - final_command = ' '.join(linux_cmds) - logger.debug("Trying to execute these commands: {}".format(final_command)) - output = subprocess.check_output(final_command, stderr=subprocess.STDOUT, shell=True) - PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_LINUX_FORMAT.format(commandline, username, output[:150]), True)).send() - return - except subprocess.CalledProcessError as e: - PostBreachTelem(self, (e.output, False)).send() - return + self.communicate_as_new_user_linux(username) - def try_to_create_user_windows(self, username, password): + def communicate_as_new_user_linux(self, username): try: - windows_cmds = BackdoorUser.get_windows_commands_to_add_user(username, password, True) - logger.debug("Trying these commands: {}".format(str(windows_cmds))) - subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) - return True - except subprocess.CalledProcessError as e: + linux_cmds = BackdoorUser.get_linux_commands_to_add_user(username) + commandline = "ping -c 2 google.com" + linux_cmds.extend([";", "sudo", "-u", username, commandline]) + final_command = ' '.join(linux_cmds) + logger.debug("Trying to execute these commands: {}".format(final_command)) + output = subprocess.check_output(final_command, stderr=subprocess.STDOUT, shell=True) PostBreachTelem(self, ( - "Couldn't create the user '{}'. Error output is: '{}'".format(username, e.output), False)).send() - return False + CREATED_PROCESS_AS_USER_LINUX_FORMAT.format(commandline, username, output[:150]), True)).send() + 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 NewUser(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, "google.com", "-n", "2") + process_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 + ) + + PostBreachTelem(self, + (CREATED_PROCESS_AS_USER_WINDOWS_FORMAT.format(commandline, username), True)).send() + + win32api.CloseHandle(process_handle[0]) # Process handle + win32api.CloseHandle(process_handle[1]) # Thread handle + + except Exception as e: + # TODO: if failed on 1314, we can try to add elevate the rights of the current user with the + # "Replace a process level token" right, using Local Security Policy editing. Worked, but only + # after reboot. So: + # 1. need to decide if worth it, and then + # 2. need to find how to do this using python... + PostBreachTelem(self, ( + "Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send() + + # Nothing more we can do. Leak the process handle. + 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()