From c8a742674f1109058e2512f4c652dfaf0224577d Mon Sep 17 00:00:00 2001 From: Shay Nehmad <shay.nehmad@guardicore.com> Date: Mon, 23 Dec 2019 17:41:42 +0200 Subject: [PATCH 1/3] Replaced ping with curl/wget and Invoke-WebRequest --- .../actions/communicate_as_new_user.py | 39 +++++++++++-------- .../infection_monkey/utils/windows/users.py | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) 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 2a1a79b91..1e4ce4e66 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 @@ -10,10 +10,10 @@ 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 -PING_TEST_DOMAIN = "google.com" +INFECTION_MONKEY_WEBSITE_URL = "https://infectionmonkey.com" -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 {})." +CREATED_PROCESS_AS_USER_SUCCESS_FORMAT = "Created process '{}' as user '{}' and the process succeeded." +CREATED_PROCESS_AS_USER_FAILED_FORMAT = "Created process '{}' as user '{}', but the process failed (exit status {})." USERNAME_PREFIX = "somenewuser" PASSWORD = "N3WPa55W0rD!1" @@ -23,8 +23,8 @@ logger = logging.getLogger(__name__) class CommunicateAsNewUser(PBA): """ - This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People - pillar. See the relevant telemetry processing to see what findings are created. + This PBA creates a new user, and then creates HTTP/S requests as that user. This is used for a Zero Trust test of the + People pillar. See the relevant telemetry processing to see what findings are created. """ def __init__(self): @@ -34,9 +34,9 @@ class CommunicateAsNewUser(PBA): username = CommunicateAsNewUser.get_random_new_user_name() 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) + http_request_commandline = CommunicateAsNewUser.get_commandline_for_http_request(INFECTION_MONKEY_WEBSITE_URL) + exit_status = new_user.run_as(http_request_commandline) + self.send_result_telemetry(exit_status, http_request_commandline, username) except subprocess.CalledProcessError as e: PostBreachTelem(self, (e.output.decode(), False)).send() except NewUserError as e: @@ -47,21 +47,28 @@ class CommunicateAsNewUser(PBA): return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) @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 get_commandline_for_http_request(url, is_windows=is_windows_os()): + if is_windows: + format_string = "powershell -command \"Invoke-WebRequest {url}\"" + else: + # true || false -> 0. false || true -> 0. false || false -> 1. So: + # if curl works, we're good. + # If curl doesn't exist or fails and wget work, we're good. + # And if both don't exist: we'll call it a win. + format_string = "curl {url} || wget -O/dev/null -q {url}" + return format_string.format(url=url) - def send_ping_result_telemetry(self, exit_status, commandline, username): + def send_result_telemetry(self, exit_status, commandline, username): """ - Parses the result of ping and sends telemetry accordingly. + Parses the result of the command and sends telemetry accordingly. - :param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success. + :param exit_status: In both Windows and Linux, 0 exit code indicates success. :param commandline: Exact commandline which was executed, for reporting back. :param username: Username from which the command was executed, for reporting back. """ if exit_status == 0: PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send() + CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True)).send() else: PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send() + CREATED_PROCESS_AS_USER_FAILED_FORMAT.format(commandline, username, exit_status), False)).send() diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py index cf6eb73c4..c6e46a690 100644 --- a/monkey/infection_monkey/utils/windows/users.py +++ b/monkey/infection_monkey/utils/windows/users.py @@ -65,7 +65,7 @@ class AutoNewWindowsUser(AutoNewUser): self.username, ".", # Use current domain. self.password, - win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). Need this to open ping + win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). # using a shell. win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. except Exception as err: From ab57b5895e40b0c763555517c2c12c6a56b80c1a Mon Sep 17 00:00:00 2001 From: Shay Nehmad <shay.nehmad@guardicore.com> Date: Tue, 24 Dec 2019 19:08:52 +0200 Subject: [PATCH 2/3] Replaced CreateProcessAsUser with CreateProcessWithLogonW to get over the "0xc0000142" error code and added -UseBasicParsing to get over the IE Engine inconfigured error This will be heavily documented in a tweet sometime --- .../actions/communicate_as_new_user.py | 13 +++++--- monkey/infection_monkey/requirements.txt | 1 + .../infection_monkey/utils/windows/users.py | 32 ++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) 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 1e4ce4e66..4a10abdc2 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 @@ -10,10 +10,10 @@ 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 -INFECTION_MONKEY_WEBSITE_URL = "https://infectionmonkey.com" +INFECTION_MONKEY_WEBSITE_URL = "https://infectionmonkey.com/" CREATED_PROCESS_AS_USER_SUCCESS_FORMAT = "Created process '{}' as user '{}' and the process succeeded." -CREATED_PROCESS_AS_USER_FAILED_FORMAT = "Created process '{}' as user '{}', but the process failed (exit status {})." +CREATED_PROCESS_AS_USER_FAILED_FORMAT = "Created process '{}' as user '{}', but the process failed (exit status {}:{})." USERNAME_PREFIX = "somenewuser" PASSWORD = "N3WPa55W0rD!1" @@ -49,7 +49,7 @@ class CommunicateAsNewUser(PBA): @staticmethod def get_commandline_for_http_request(url, is_windows=is_windows_os()): if is_windows: - format_string = "powershell -command \"Invoke-WebRequest {url}\"" + format_string = 'powershell.exe -command "Invoke-WebRequest {url}" -UseBasicParsing' else: # true || false -> 0. false || true -> 0. false || false -> 1. So: # if curl works, we're good. @@ -71,4 +71,9 @@ class CommunicateAsNewUser(PBA): CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True)).send() else: PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_FAILED_FORMAT.format(commandline, username, exit_status), False)).send() + CREATED_PROCESS_AS_USER_FAILED_FORMAT.format( + commandline, username, exit_status, twos_complement(exit_status)), False)).send() + + +def twos_complement(exit_status): + return hex(exit_status & (2 ** 32 - 1)) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 93b3b4ca3..7a75c1b7e 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -13,3 +13,4 @@ wmi pywin32 ; sys_platform == 'win32' pymssql<3.0 pyftpdlib +WinSys-3.x diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py index c6e46a690..fcd364032 100644 --- a/monkey/infection_monkey/utils/windows/users.py +++ b/monkey/infection_monkey/utils/windows/users.py @@ -5,7 +5,7 @@ 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 +WAIT_TIMEOUT_IN_MILLISECONDS = 60 * 1000 logger = logging.getLogger(__name__) @@ -65,8 +65,7 @@ class AutoNewWindowsUser(AutoNewUser): self.username, ".", # Use current domain. self.password, - win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). - # using a shell. + win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user), since we're 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))) @@ -78,33 +77,28 @@ class AutoNewWindowsUser(AutoNewUser): import win32process import win32api import win32event + from winsys import _advapi32 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 + # Open process as that user + # https://github.com/tjguk/winsys/blob/master/winsys/_advapi32.py + proc_info = _advapi32.CreateProcessWithLogonW( + username=self.username, + domain=".", + password=self.password, + command_line=command ) + process_handle = proc_info.hProcess + thread_handle = proc_info.hThread logger.debug( "Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS)) + # https://social.msdn.microsoft.com/Forums/vstudio/en-US/b6d6a7ae-71e9-4edb-ac8f-408d2a41750d/what-events-on-a-process-handle-signal-satisify-waitforsingleobject?forum=vcgeneral # 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 From 6e9ee4158d356c0c136a8c74480efaf94e521d5c Mon Sep 17 00:00:00 2001 From: Shay Nehmad <shay.nehmad@guardicore.com> Date: Wed, 25 Dec 2019 10:57:17 +0200 Subject: [PATCH 3/3] We chose HTTPS --- .../post_breach/actions/communicate_as_new_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4a10abdc2..42162fb55 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 @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) class CommunicateAsNewUser(PBA): """ - This PBA creates a new user, and then creates HTTP/S requests as that user. This is used for a Zero Trust test of the + This PBA creates a new user, and then creates HTTPS requests as that user. This is used for a Zero Trust test of the People pillar. See the relevant telemetry processing to see what findings are created. """