diff --git a/monkey/common/cmd/aws/__init__.py b/monkey/common/cmd/aws/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/cmd/aws_cmd_result.py b/monkey/common/cmd/aws/aws_cmd_result.py similarity index 100% rename from monkey/common/cmd/aws_cmd_result.py rename to monkey/common/cmd/aws/aws_cmd_result.py diff --git a/monkey/common/cmd/aws_cmd_runner.py b/monkey/common/cmd/aws/aws_cmd_runner.py similarity index 80% rename from monkey/common/cmd/aws_cmd_runner.py rename to monkey/common/cmd/aws/aws_cmd_runner.py index 10927cb81..b4198f642 100644 --- a/monkey/common/cmd/aws_cmd_runner.py +++ b/monkey/common/cmd/aws/aws_cmd_runner.py @@ -1,9 +1,7 @@ -import time import logging from common.cloud.aws_service import AwsService -from common.cmd.aws_cmd_result import AwsCmdResult -from common.cmd.cmd_result import CmdResult +from common.cmd.aws.aws_cmd_result import AwsCmdResult from common.cmd.cmd_runner import CmdRunner from common.cmd.cmd_status import CmdStatus @@ -14,10 +12,10 @@ logger = logging.getLogger(__name__) class AwsCmdRunner(CmdRunner): """ - Class for running a command on a remote AWS machine + Class for running commands on a remote AWS machine """ - def __init__(self, instance_id, region, is_linux): + def __init__(self, is_linux, instance_id, region = None): super(AwsCmdRunner, self).__init__(is_linux) self.instance_id = instance_id self.region = region @@ -37,8 +35,8 @@ class AwsCmdRunner(CmdRunner): else: return CmdStatus.FAILURE - def run_command_async(self, command): + def run_command_async(self, command_line): doc_name = "AWS-RunShellScript" if self.is_linux else "AWS-RunPowerShellScript" - command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command]}, + command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command_line]}, InstanceIds=[self.instance_id]) return command_res['Command']['CommandId'] diff --git a/monkey/common/cmd/cmd.py b/monkey/common/cmd/cmd.py new file mode 100644 index 000000000..8cb2177a2 --- /dev/null +++ b/monkey/common/cmd/cmd.py @@ -0,0 +1,11 @@ +__author__ = 'itay.mizeretz' + + +class Cmd(object): + """ + Class representing a command + """ + + def __init__(self, cmd_runner, cmd_id): + self.cmd_runner = cmd_runner + self.cmd_id = cmd_id diff --git a/monkey/common/cmd/cmd_result.py b/monkey/common/cmd/cmd_result.py index 40eca2c85..d3039736f 100644 --- a/monkey/common/cmd/cmd_result.py +++ b/monkey/common/cmd/cmd_result.py @@ -1,3 +1,4 @@ +__author__ = 'itay.mizeretz' class CmdResult(object): diff --git a/monkey/common/cmd/cmd_runner.py b/monkey/common/cmd/cmd_runner.py index c0541cc0b..6686508a4 100644 --- a/monkey/common/cmd/cmd_runner.py +++ b/monkey/common/cmd/cmd_runner.py @@ -2,6 +2,7 @@ import time import logging from abc import abstractmethod +from common.cmd.cmd import Cmd from common.cmd.cmd_result import CmdResult from common.cmd.cmd_status import CmdStatus @@ -12,7 +13,17 @@ logger = logging.getLogger(__name__) class CmdRunner(object): """ - Interface for running a command on a remote machine + Interface for running commands on a remote machine + + Since these classes are a bit complex, I provide a list of common terminology and formats: + * command line - a command line. e.g. 'echo hello' + * command - represent a single command which was already run. Always of type Cmd + * command id - any unique identifier of a command which was already run + * command result - represents the result of running a command. Always of type CmdResult + * command status - represents the current status of a command. Always of type CmdStatus + * command info - Any consistent structure representing additional information of a command which was already run + * instance - a machine that commands will be run on. Can be any dictionary with 'instance_id' as a field + * instance_id - any unique identifier of an instance (machine). Can be of any format """ # Default command timeout in seconds @@ -23,21 +34,45 @@ class CmdRunner(object): def __init__(self, is_linux): self.is_linux = is_linux - def run_command(self, command, timeout=DEFAULT_TIMEOUT): + def run_command(self, command_line, timeout=DEFAULT_TIMEOUT): """ Runs the given command on the remote machine - :param command: The command to run + :param command_line: The command line to run :param timeout: Timeout in seconds for command. :return: Command result """ - c_id = self.run_command_async(command) - self.wait_commands([(self, c_id)], timeout) + c_id = self.run_command_async(command_line) + return self.wait_commands([Cmd(self, c_id)], timeout)[1] + + @staticmethod + def run_multiple_commands(instances, inst_to_cmd, inst_n_cmd_res_to_res): + """ + Run multiple commands on various instances + :param instances: List of instances. + :param inst_to_cmd: Function which receives an instance, runs a command asynchronously and returns Cmd + :param inst_n_cmd_res_to_res: Function which receives an instance and CmdResult + and returns a parsed result (of any format) + :return: Dictionary with 'instance_id' as key and parsed result as value + """ + command_instance_dict = {} + + for instance in instances: + command = inst_to_cmd(instance) + command_instance_dict[command] = instance + + instance_results = {} + command_result_pairs = CmdRunner.wait_commands(command_instance_dict.keys()) + for command, result in command_result_pairs: + instance = command_instance_dict[command] + instance_results[instance['instance_id']] = inst_n_cmd_res_to_res(instance, result) + + return instance_results @abstractmethod - def run_command_async(self, command): + def run_command_async(self, command_line): """ Runs the given command on the remote machine asynchronously. - :param command: The command to run + :param command_line: The command line to run :return: Command ID (in any format) """ raise NotImplementedError() @@ -46,9 +81,9 @@ class CmdRunner(object): def wait_commands(commands, timeout=DEFAULT_TIMEOUT): """ Waits on all commands up to given timeout - :param commands: list of tuples of command IDs and command runners + :param commands: list of commands (of type Cmd) :param timeout: Timeout in seconds for command. - :return: commands' results (tuple of + :return: commands and their results (tuple of Command and CmdResult) """ init_time = time.time() curr_time = init_time @@ -56,7 +91,7 @@ class CmdRunner(object): results = [] while (curr_time - init_time < timeout) and (len(commands) != 0): - for command in list(commands): + for command in list(commands): # list(commands) clones the list. We do so because we remove items inside CmdRunner._process_command(command, commands, results, True) time.sleep(CmdRunner.WAIT_SLEEP_TIME) @@ -109,8 +144,8 @@ class CmdRunner(object): :param should_process_only_finished: If True, processes only if command finished. :return: None """ - c_runner = command[0] - c_id = command[1] + c_runner = command.cmd_runner + c_id = command.cmd_id try: command_info = c_runner.query_command(c_id) if (not should_process_only_finished) or c_runner.get_command_status(command_info) != CmdStatus.IN_PROGRESS: diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index b4da8eaf2..08a3cb157 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -3,116 +3,30 @@ from flask import request, jsonify, make_response import flask_restful from cc.auth import jwt_required -from cc.services.config import ConfigService -from common.cloud.aws_instance import AwsInstance +from cc.services.remote_run_aws import RemoteRunAwsService from common.cloud.aws_service import AwsService -from common.cmd.aws_cmd_runner import AwsCmdRunner -from common.cmd.cmd_runner import CmdRunner class RemoteRun(flask_restful.Resource): def __init__(self): super(RemoteRun, self).__init__() - self.aws_instance = AwsInstance() + RemoteRunAwsService.init() def run_aws_monkeys(self, request_body): - self.init_aws_auth_params() instances = request_body.get('instances') island_ip = request_body.get('island_ip') - instances_bitness = self.get_bitness(instances) - return self.run_multiple_commands( - instances, - lambda instance: self.run_aws_monkey_cmd_async(instance['instance_id'], - instance['os'], island_ip, instances_bitness[instance['instance_id']]), - lambda _, result: result.is_success) - - def run_multiple_commands(self, instances, inst_to_cmd, inst_n_cmd_res_to_res): - command_instance_dict = {} - - for instance in instances: - command = inst_to_cmd(instance) - command_instance_dict[command] = instance - - instance_results = {} - results = CmdRunner.wait_commands(command_instance_dict.keys()) - for command, result in results: - instance = command_instance_dict[command] - instance_results[instance['instance_id']] = inst_n_cmd_res_to_res(instance, result) - - return instance_results - - def get_bitness(self, instances): - return self.run_multiple_commands( - instances, - lambda instance: RemoteRun.run_aws_bitness_cmd_async(instance['instance_id'], instance['os']), - lambda instance, result: self.get_bitness_by_result('linux' == instance['os'], result)) - - def get_bitness_by_result(self, is_linux, result): - if not result.is_success: - return None - elif is_linux: - return result.stdout.find('i686') == -1 # i686 means 32bit - else: - return result.stdout.lower().find('programfiles(x86)') != -1 # if not found it means 32bit - - @staticmethod - def run_aws_bitness_cmd_async(instance_id, os): - """ - Runs an AWS command to check bitness - :param instance_id: Instance ID of target - :param os: OS of target ('linux' or 'windows') - :return: Tuple of CmdRunner and command id - """ - is_linux = ('linux' == os) - cmd = AwsCmdRunner(instance_id, None, is_linux) - cmd_text = 'uname -m' if is_linux else 'Get-ChildItem Env:' - return cmd, cmd.run_command_async(cmd_text) - - def run_aws_monkey_cmd_async(self, instance_id, os, island_ip, is_64bit): - """ - Runs a monkey remotely using AWS - :param instance_id: Instance ID of target - :param os: OS of target ('linux' or 'windows') - :param island_ip: IP of the island which the instance will try to connect to - :param is_64bit: Whether the instance is 64bit - :return: Tuple of CmdRunner and command id - """ - is_linux = ('linux' == os) - cmd = AwsCmdRunner(instance_id, None, is_linux) - cmd_text = self._get_run_monkey_cmd_line(is_linux, is_64bit, island_ip) - return cmd, cmd.run_command_async(cmd_text) - - def _get_run_monkey_cmd_linux_line(self, bit_text, island_ip): - return r'wget --no-check-certificate https://' + island_ip + r':5000/api/monkey/download/monkey-linux-' + \ - bit_text + r'; chmod +x monkey-linux-' + bit_text + r'; ./monkey-linux-' + bit_text + r' m0nk3y -s ' + \ - island_ip + r':5000' - - def _get_run_monkey_cmd_windows_line(self, bit_text, island_ip): - return r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" \ - r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + island_ip + \ - r":5000/api/monkey/download/monkey-windows-" + bit_text + r".exe','.\\monkey.exe'); " \ - r";Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " - - def _get_run_monkey_cmd_line(self, is_linux, is_64bit, island_ip): - bit_text = '64' if is_64bit else '32' - return self._get_run_monkey_cmd_linux_line(bit_text, island_ip) if is_linux \ - else self._get_run_monkey_cmd_windows_line(bit_text, island_ip) - - def init_aws_auth_params(self): - access_key_id = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_access_key_id'], False, True) - secret_access_key = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_secret_access_key'], False, True) - AwsService.set_auth_params(access_key_id, secret_access_key) - AwsService.set_region(self.aws_instance.region) + RemoteRunAwsService.update_aws_auth_params() + return RemoteRunAwsService.run_aws_monkeys(instances, island_ip) @jwt_required() def get(self): action = request.args.get('action') if action == 'list_aws': - is_aws = self.aws_instance.is_aws_instance() + is_aws = RemoteRunAwsService.is_running_on_aws() resp = {'is_aws': is_aws} if is_aws: + RemoteRunAwsService.update_aws_auth_params() resp['instances'] = AwsService.get_instances() - self.init_aws_auth_params() return jsonify(resp) return {} diff --git a/monkey/monkey_island/cc/services/remote_run_aws.py b/monkey/monkey_island/cc/services/remote_run_aws.py new file mode 100644 index 000000000..560245556 --- /dev/null +++ b/monkey/monkey_island/cc/services/remote_run_aws.py @@ -0,0 +1,131 @@ +from cc.services.config import ConfigService +from common.cloud.aws_instance import AwsInstance +from common.cloud.aws_service import AwsService +from common.cmd.aws.aws_cmd_runner import AwsCmdRunner +from common.cmd.cmd import Cmd +from common.cmd.cmd_runner import CmdRunner + +__author__ = "itay.mizeretz" + + +class RemoteRunAwsService: + aws_instance = None + + def __init__(self): + pass + + @staticmethod + def init(): + """ + Initializes service. Subsequent calls to this function have no effect. + Must be called at least once (in entire monkey lifetime) before usage of functions + :return: None + """ + if RemoteRunAwsService.aws_instance is None: + RemoteRunAwsService.aws_instance = AwsInstance() + + @staticmethod + def run_aws_monkeys(instances, island_ip): + """ + Runs monkeys on the given instances + :param instances: List of instances to run on + :param island_ip: IP of island the monkey will communicate with + :return: Dictionary with instance ids as keys, and True/False as values if succeeded or not + """ + instances_bitness = RemoteRunAwsService.get_bitness(instances) + return CmdRunner.run_multiple_commands( + instances, + lambda instance: RemoteRunAwsService.run_aws_monkey_cmd_async( + instance['instance_id'], RemoteRunAwsService._is_linux(instance['os']), island_ip, + instances_bitness[instance['instance_id']]), + lambda _, result: result.is_success) + + @staticmethod + def is_running_on_aws(): + return RemoteRunAwsService.aws_instance.is_aws_instance() + + @staticmethod + def update_aws_auth_params(): + """ + Updates the AWS authentication parameters according to config + :return: None + """ + access_key_id = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_access_key_id'], False, True) + secret_access_key = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_secret_access_key'], False, True) + AwsService.set_auth_params(access_key_id, secret_access_key) + AwsService.set_region(RemoteRunAwsService.aws_instance.region) + + @staticmethod + def get_bitness(instances): + """ + For all given instances, checks whether they're 32 or 64 bit. + :param instances: List of instances to check + :return: Dictionary with instance ids as keys, and True/False as values. True if 64bit, False otherwise + """ + return CmdRunner.run_multiple_commands( + instances, + lambda instance: RemoteRunAwsService.run_aws_bitness_cmd_async( + instance['instance_id'], RemoteRunAwsService._is_linux(instance['os'])), + lambda instance, result: RemoteRunAwsService._get_bitness_by_result( + RemoteRunAwsService._is_linux(instance['os']), result)) + + @staticmethod + def _get_bitness_by_result(is_linux, result): + if not result.is_success: + return None + elif is_linux: + return result.stdout.find('i686') == -1 # i686 means 32bit + else: + return result.stdout.lower().find('programfiles(x86)') != -1 # if not found it means 32bit + + @staticmethod + def run_aws_bitness_cmd_async(instance_id, is_linux): + """ + Runs an AWS command to check bitness + :param instance_id: Instance ID of target + :param is_linux: Whether target is linux + :return: Cmd + """ + cmd_text = 'uname -m' if is_linux else 'Get-ChildItem Env:' + return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text) + + @staticmethod + def run_aws_monkey_cmd_async(instance_id, is_linux, island_ip, is_64bit): + """ + Runs a monkey remotely using AWS + :param instance_id: Instance ID of target + :param is_linux: Whether target is linux + :param island_ip: IP of the island which the instance will try to connect to + :param is_64bit: Whether the instance is 64bit + :return: Cmd + """ + cmd_text = RemoteRunAwsService._get_run_monkey_cmd_line(is_linux, is_64bit, island_ip) + return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text) + + @staticmethod + def run_aws_cmd_async(instance_id, is_linux, cmd_line): + cmd_runner = AwsCmdRunner(is_linux, instance_id) + return Cmd(cmd_runner, cmd_runner.run_command_async(cmd_line)) + + @staticmethod + def _is_linux(os): + return 'linux' == os + + @staticmethod + def _get_run_monkey_cmd_linux_line(bit_text, island_ip): + return r'wget --no-check-certificate https://' + island_ip + r':5000/api/monkey/download/monkey-linux-' + \ + bit_text + r'; chmod +x monkey-linux-' + bit_text + r'; ./monkey-linux-' + bit_text + r' m0nk3y -s ' + \ + island_ip + r':5000' + + @staticmethod + def _get_run_monkey_cmd_windows_line(bit_text, island_ip): + return r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" \ + r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + island_ip + \ + r":5000/api/monkey/download/monkey-windows-" + bit_text + r".exe','.\\monkey.exe'); " \ + r";Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " + + @staticmethod + def _get_run_monkey_cmd_line(is_linux, is_64bit, island_ip): + bit_text = '64' if is_64bit else '32' + return RemoteRunAwsService._get_run_monkey_cmd_linux_line(bit_text, island_ip) if is_linux \ + else RemoteRunAwsService._get_run_monkey_cmd_windows_line(bit_text, island_ip)