diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py index 8262757ca..dee4f67d0 100644 --- a/monkey/common/data/post_breach_consts.py +++ b/monkey/common/data/post_breach_consts.py @@ -1,2 +1,3 @@ +POST_BREACH_COMMUNICATE_AS_NEW_USER = "Communicate as new user" POST_BREACH_BACKDOOR_USER = "Backdoor user" POST_BREACH_FILE_EXECUTION = "File execution" diff --git a/monkey/common/data/zero_trust_consts.py b/monkey/common/data/zero_trust_consts.py index 3cf9cda92..9a452d706 100644 --- a/monkey/common/data/zero_trust_consts.py +++ b/monkey/common/data/zero_trust_consts.py @@ -30,6 +30,7 @@ TEST_SCHEDULED_EXECUTION = u"scheduled_execution" TEST_MALICIOUS_ACTIVITY_TIMELINE = u"malicious_activity_timeline" TEST_SEGMENTATION = u"segmentation" TEST_TUNNELING = u"tunneling" +TEST_COMMUNICATE_AS_NEW_USER = u"communicate_as_new_user" TESTS = ( TEST_SEGMENTATION, TEST_MALICIOUS_ACTIVITY_TIMELINE, @@ -38,7 +39,8 @@ TESTS = ( TEST_MACHINE_EXPLOITED, TEST_DATA_ENDPOINT_HTTP, TEST_DATA_ENDPOINT_ELASTIC, - TEST_TUNNELING + TEST_TUNNELING, + TEST_COMMUNICATE_AS_NEW_USER ) RECOMMENDATION_DATA_TRANSIT = u"data_transit" @@ -47,13 +49,15 @@ RECOMMENDATION_USER_BEHAVIOUR = u"user_behaviour" RECOMMENDATION_ANALYZE_NETWORK_TRAFFIC = u"analyze_network_traffic" RECOMMENDATION_SEGMENTATION = u"segmentation" RECOMMENDATION_RESTRICTIVE_NETWORK_POLICIES = u"network_policies" +RECOMMENDATION_USERS_MAC_POLICIES = u"users_mac_policies" RECOMMENDATIONS = { RECOMMENDATION_SEGMENTATION: u"Apply segmentation and micro-segmentation inside your network.", RECOMMENDATION_ANALYZE_NETWORK_TRAFFIC: u"Analyze network traffic for malicious activity.", RECOMMENDATION_USER_BEHAVIOUR: u"Adopt security user behavior analytics.", RECOMMENDATION_ENDPOINT_SECURITY: u"Use anti-virus and other traditional endpoint security solutions.", RECOMMENDATION_DATA_TRANSIT: u"Secure data at transit by encrypting it.", - RECOMMENDATION_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible." + RECOMMENDATION_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible.", + RECOMMENDATION_USERS_MAC_POLICIES: u"Users' permissions to the network and to resources should be MAC only.", } POSSIBLE_STATUSES_KEY = u"possible_statuses" @@ -140,6 +144,16 @@ TESTS_MAP = { PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED] }, + TEST_COMMUNICATE_AS_NEW_USER: { + TEST_EXPLANATION_KEY: u"The Monkey tried create a new user and communicate with the internet from it.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey was able to cause a new user to access the network. Your network policies are too permissive - restrict them to MAC only.", + STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network." + }, + RECOMMENDATION_KEY: RECOMMENDATION_USERS_MAC_POLICIES, + PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, } EVENT_TYPE_ISLAND = "island" diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py index ce05371a6..b217196d3 100644 --- a/monkey/infection_monkey/post_breach/actions/add_user.py +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -17,6 +17,40 @@ WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, class BackdoorUser(PBA): def __init__(self): - super(BackdoorUser, self).__init__(POST_BREACH_BACKDOOR_USER, - linux_cmd=' '.join(LINUX_COMMANDS), - windows_cmd=WINDOWS_COMMANDS) + linux_cmds, windows_cmds = BackdoorUser.get_commands_to_add_user( + WormConfiguration.user_to_add, WormConfiguration.remote_user_pass) + super(BackdoorUser, self).__init__( + POST_BREACH_BACKDOOR_USER, + linux_cmd=' '.join(linux_cmds), + windows_cmd=windows_cmds) + + @staticmethod + def get_commands_to_add_user(username, password): + linux_cmds = BackdoorUser.get_linux_commands_to_add_user(username) + windows_cmds = BackdoorUser.get_windows_commands_to_add_user(password, username) + return linux_cmds, windows_cmds + + @staticmethod + def get_linux_commands_to_add_user(username): + linux_cmds = [ + 'useradd', + '-M', + '--expiredate', + datetime.datetime.today().strftime('%Y-%m-%d'), + '--inactive', + '0', + '-c', + 'MONKEY_USER', + username] + return linux_cmds + + @staticmethod + def get_windows_commands_to_add_user(password, username): + windows_cmds = [ + 'net', + 'user', + username, + password, + '/add', + '/ACTIVE:NO'] + return windows_cmds 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 new file mode 100644 index 000000000..63daa614c --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py @@ -0,0 +1,98 @@ +import os +import random +import string +import subprocess + +import win32api +import win32con +import win32process +import win32security + +from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +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 +from infection_monkey.utils import is_windows_os + +USERNAME = "somenewuser" +PASSWORD = "N3WPa55W0rD!@12" + + +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. + """ + + def __init__(self): + super(CommunicateAsNewUser, self).__init__(name=POST_BREACH_COMMUNICATE_AS_NEW_USER) + + def run(self): + username = USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) + if is_windows_os(): + if not self.try_to_create_user_windows(username, PASSWORD): + return # no point to continue if failed creating the user. + + # 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, + ".", # current domain + PASSWORD, + win32con.LOGON32_LOGON_BATCH, # logon type + win32con.LOGON32_PROVIDER_DEFAULT) # logon provider + + if new_user_logon_token_handle == 0: + PostBreachTelem( + self, + ("Can't logon as {} Last error: {}".format(username, win32api.GetLastError()), 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. + + # Open process as that user: + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera + return_value_create_process = win32process.CreateProcessAsUser( + new_user_logon_token_handle, # A handle to the primary token that represents a user. + # If both lpApplicationName and lpCommandLine are non-NULL, *lpApplicationName specifies the module + # to execute, and *lpCommandLine specifies the command line. + ping_app_path, # The name of the module to be executed. + "google.com", # 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 + ) + + if return_value_create_process == 0: + PostBreachTelem(self, ( + "Failed to open process as user. Last error: {}".format(win32api.GetLastError()), False)).send() + return + else: + try: + linux_cmds = BackdoorUser.get_linux_commands_to_add_user(username) + linux_cmds.extend([";", "sudo", "-", username, "-c", "'ping -c 2 google.com'"]) + subprocess.check_output(linux_cmds, stderr=subprocess.STDOUT, shell=True) + except subprocess.CalledProcessError as e: + PostBreachTelem(self, (e.output, False)).send() + return + + def try_to_create_user_windows(self, username, password): + try: + windows_cmds = BackdoorUser.get_windows_commands_to_add_user(username, password) + subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) + return True + except subprocess.CalledProcessError as e: + PostBreachTelem(self, ( + "Couldn't create the user '{}'. Error output is: '{}'".format(username, e.output), False)).send() + return False diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 3fb8b251f..6c6c6b8b5 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -19,7 +19,8 @@ class PBA(object): def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. - :param command: Command that will be executed on breached machine + :param linux_cmd: Command that will be executed on breached machine + :param windows_cmd: Command that will be executed on breached machine """ self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 8522f412f..7c5bea27d 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -25,8 +25,9 @@ class PostBreach(object): Executes all post breach actions. """ for pba in self.pba_list: + LOG.debug("Executing PBA: '{}'".format(pba.name)) pba.run() - LOG.info("Post breach actions executed") + LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) @staticmethod def config_to_pba_list(): diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 3a7398663..67b5b519d 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -119,6 +119,14 @@ SCHEMA = { "title": "Back door user", "attack_techniques": [] }, + { + "type": "string", + "enum": [ + "CommunicateAsNewUser" + ], + "title": "Communicate as new user", + "attack_techniques": [] + }, ], }, "finger_classes": { diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 2515c2d30..c67f64f59 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -1,7 +1,18 @@ from monkey_island.cc.database import mongo from common.data.post_breach_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.zero_trust_tests.communicate_as_new_user import test_new_user_communication + + +def process_communicate_as_new_user_telemetry(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + success = telemetry_json['data']['result'][1] + message = telemetry_json['data']['result'][0] + test_new_user_communication(current_monkey, success, message) + POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { + POST_BREACH_COMMUNICATE_AS_NEW_USER: process_communicate_as_new_user_telemetry, # `lambda *args, **kwargs: None` is a no-op. POST_BREACH_BACKDOOR_USER: lambda *args, **kwargs: None, POST_BREACH_FILE_EXECUTION: lambda *args, **kwargs: None, @@ -13,5 +24,7 @@ def process_post_breach_telemetry(telemetry_json): {'guid': telemetry_json['monkey_guid']}, {'$push': {'pba_results': telemetry_json['data']}}) - if telemetry_json["name"] in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: - POST_BREACH_TELEMETRY_PROCESSING_FUNCS[telemetry_json["name"]](telemetry_json) + post_breach_action_name = telemetry_json["data"]["name"] + if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: + POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json) + diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py new file mode 100644 index 000000000..564ce4d20 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py @@ -0,0 +1,35 @@ +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK, STATUS_FAILED, TEST_COMMUNICATE_AS_NEW_USER, \ + STATUS_PASSED +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event + + +def test_new_user_communication(current_monkey, success, message): + tried_to_communicate_event = Event.create_event( + title="Communicate as new user", + message="Monkey on {} tried to create a new user and communicate from it.".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_NETWORK) + events = [tried_to_communicate_event] + + if success: + events.append( + Event.create_event( + title="Communicate as new user", + message="New user created by Monkey on {} successfully tried to communicate with the internet. " + "Details: {}".format(current_monkey.hostname, message), + event_type=EVENT_TYPE_MONKEY_NETWORK) + ) + test_status = STATUS_FAILED + else: + events.append( + Event.create_event( + title="Communicate as new user", + message="Monkey on {} couldn't communicate as new user. Details: {}".format( + current_monkey.hostname, message), + event_type=EVENT_TYPE_MONKEY_NETWORK) + ) + test_status = STATUS_PASSED + + AggregateFinding.create_or_add_to_existing( + test=TEST_COMMUNICATE_AS_NEW_USER, status=test_status, events=events + )