diff --git a/monkey/common/cloud/aws_service.py b/monkey/common/cloud/aws_service.py index 0cc641356..351c032f5 100644 --- a/monkey/common/cloud/aws_service.py +++ b/monkey/common/cloud/aws_service.py @@ -8,7 +8,6 @@ class AwsService(object): Supplies various AWS services """ - # TODO: consider changing from static to singleton, and generally change design access_key_id = None secret_access_key = None region = None @@ -39,3 +38,17 @@ class AwsService(object): @staticmethod def get_regions(): return AwsService.get_session().get_available_regions('ssm') + + @staticmethod + def get_instances(): + return \ + [ + { + 'instance_id': x['InstanceId'], + 'name': x['ComputerName'], + 'os': x['PlatformType'].lower(), + 'ip_address': x['IPAddress'] + } + for x in AwsService.get_client('ssm').describe_instance_information()['InstanceInformationList'] + ] + diff --git a/monkey/common/cmd/aws_cmd_result.py b/monkey/common/cmd/aws_cmd_result.py index 5c9057a61..79b1bb79d 100644 --- a/monkey/common/cmd/aws_cmd_result.py +++ b/monkey/common/cmd/aws_cmd_result.py @@ -11,10 +11,16 @@ class AwsCmdResult(CmdResult): def __init__(self, command_info): super(AwsCmdResult, self).__init__( - self.is_successful(command_info), command_info[u'ResponseCode'], command_info[u'StandardOutputContent'], + self.is_successful(command_info, True), command_info[u'ResponseCode'], command_info[u'StandardOutputContent'], command_info[u'StandardErrorContent']) self.command_info = command_info @staticmethod - def is_successful(command_info): - return command_info[u'Status'] == u'Success' + def is_successful(command_info, is_timeout=False): + """ + Determines whether the command was successful. If it timed out and was still in progress, we assume it worked. + :param command_info: Command info struct (returned by ssm.get_command_invocation) + :param is_timeout: Whether the given command timed out + :return: True if successful, False otherwise. + """ + return (command_info[u'Status'] == u'Success') or (is_timeout and (command_info[u'Status'] == u'InProgress')) diff --git a/monkey/common/cmd/aws_cmd_runner.py b/monkey/common/cmd/aws_cmd_runner.py index 0f5032b9d..1b2d01e42 100644 --- a/monkey/common/cmd/aws_cmd_runner.py +++ b/monkey/common/cmd/aws_cmd_runner.py @@ -1,39 +1,56 @@ import time +import logging +import json 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.cmd_runner import CmdRunner __author__ = 'itay.mizeretz' +logger = logging.getLogger(__name__) -class AwsCmdRunner(object): + +class AwsCmdRunner(CmdRunner): """ Class for running a command on a remote AWS machine """ - def __init__(self, instance_id, region, is_powershell=False): + def __init__(self, instance_id, region, is_linux): + super(AwsCmdRunner, self).__init__(is_linux) self.instance_id = instance_id self.region = region - self.is_powershell = is_powershell self.ssm = AwsService.get_client('ssm', region) - def run_command(self, command, timeout): + def run_command(self, command, timeout=CmdRunner.DEFAULT_TIMEOUT): + # TODO: document command_id = self._send_command(command) init_time = time.time() curr_time = init_time command_info = None - while curr_time - init_time < timeout: - command_info = self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id) - if AwsCmdResult.is_successful(command_info): - break - else: - time.sleep(0.5) - curr_time = time.time() - return AwsCmdResult(command_info) + try: + while curr_time - init_time < timeout: + command_info = self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id) + if AwsCmdResult.is_successful(command_info): + break + else: + time.sleep(0.5) + curr_time = time.time() + + cmd_res = AwsCmdResult(command_info) + + if not cmd_res.is_success: + logger.error('Failed running AWS command: `%s`. status code: %s', command, str(cmd_res.status_code)) + + return cmd_res + except Exception: + logger.exception('Exception while running AWS command: `%s`', command) + return CmdResult(False) def _send_command(self, command): - doc_name = "AWS-RunPowerShellScript" if self.is_powershell else "AWS-RunShellScript" + doc_name = "AWS-RunShellScript" if self.is_linux else "AWS-RunPowerShellScript" command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command]}, InstanceIds=[self.instance_id]) return command_res['Command']['CommandId'] diff --git a/monkey/common/cmd/cmd_runner.py b/monkey/common/cmd/cmd_runner.py index cf4afa289..1875b7d4e 100644 --- a/monkey/common/cmd/cmd_runner.py +++ b/monkey/common/cmd/cmd_runner.py @@ -11,6 +11,9 @@ class CmdRunner(object): # Default command timeout in seconds DEFAULT_TIMEOUT = 5 + def __init__(self, is_linux): + self.is_linux = is_linux + @abstractmethod def run_command(self, command, timeout=DEFAULT_TIMEOUT): """ @@ -20,3 +23,19 @@ class CmdRunner(object): :return: Command result """ raise NotImplementedError() + + def is_64bit(self): + """ + Runs a command to determine whether OS is 32 or 64 bit. + :return: True if 64bit, False if 32bit, None if failed. + """ + if self.is_linux: + cmd_result = self.run_command('uname -m') + if not cmd_result.is_success: + return None + return cmd_result.stdout.find('i686') == -1 # i686 means 32bit + else: + cmd_result = self.run_command('Get-ChildItem Env:') + if not cmd_result.is_success: + return None + return cmd_result.stdout.lower().find('programfiles(x86)') != -1 # if not found it means 32bit diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 5bb94b611..ef8410ed9 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -22,6 +22,7 @@ from cc.resources.island_configuration import IslandConfiguration from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap from cc.resources.node import Node +from cc.resources.remote_run import RemoteRun from cc.resources.report import Report from cc.resources.root import Root from cc.resources.telemetry import Telemetry @@ -115,5 +116,6 @@ def init_app(mongo_url): api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/') + api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') return app diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index 6df5aee02..d4ebbed0b 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -2,19 +2,92 @@ import json 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 common.cloud.aws_service import AwsService +from common.cmd.aws_cmd_runner import AwsCmdRunner + class RemoteRun(flask_restful.Resource): - def run_aws_monkey(self, request_body): - instance_id = request_body.get('instance_id') - region = request_body.get('region') - os = request_body.get('os') # TODO: consider getting this from instance - island_ip = request_body.get('island_ip') # TODO: Consider getting this another way. Not easy to determine target interface + def __init__(self): + super(RemoteRun, self).__init__() + self.aws_instance = AwsInstance() + def run_aws_monkeys(self, request_body): + self.init_aws_auth_params() + instances = request_body.get('instances') + island_ip = request_body.get('island_ip') + + results = {} + + for instance in instances: + is_success = self.run_aws_monkey_cmd(instance['instance_id'], instance['os'], island_ip) + results[instance['instance_id']] = is_success + + return results + + def run_aws_monkey_cmd(self, instance_id, os, island_ip): + """ + 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 + :return: True if successfully ran monkey, False otherwise. + """ + is_linux = ('linux' == os) + cmd = AwsCmdRunner(instance_id, None, is_linux) + is_64bit = cmd.is_64bit() + cmd_text = self._get_run_monkey_cmd(is_linux, is_64bit, island_ip) + return cmd.run_command(cmd_text).is_success + + def _get_run_monkey_cmd_linux(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' + """ + return r'curl -O -k 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(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(self, is_linux, is_64bit, island_ip): + bit_text = '64' if is_64bit else '32' + return self._get_run_monkey_cmd_linux(bit_text, island_ip) if is_linux \ + else self._get_run_monkey_cmd_windows(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) + + @jwt_required() + def get(self): + action = request.args.get('action') + if action == 'list_aws': + is_aws = self.aws_instance.is_aws_instance() + resp = {'is_aws': is_aws} + if is_aws: + resp['instances'] = AwsService.get_instances() + self.init_aws_auth_params() + return jsonify(resp) + + return {} + + @jwt_required() def post(self): body = json.loads(request.data) if body.get('type') == 'aws': - local_run = self.run_aws_monkey(body) - return jsonify(is_running=local_run[0], error_text=local_run[1]) + result = self.run_aws_monkeys(body) + return jsonify({'result': result}) # default action - return make_response({'error': 'Invalid action'}, 500) \ No newline at end of file + return make_response({'error': 'Invalid action'}, 500) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js index 5c93065c4..e90292406 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js @@ -4,6 +4,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'; import {Icon} from 'react-fa'; import {Link} from 'react-router-dom'; import AuthComponent from '../AuthComponent'; +import AwsRunTable from "../run-monkey/AwsRunTable"; class RunMonkeyPageComponent extends AuthComponent { @@ -13,9 +14,13 @@ class RunMonkeyPageComponent extends AuthComponent { ips: [], runningOnIslandState: 'not_running', runningOnClientState: 'not_running', + awsClicked: false, selectedIp: '0.0.0.0', selectedOs: 'windows-32', - showManual: false + showManual: false, + showAws: false, + isOnAws: false, + awsMachines: [] }; } @@ -37,6 +42,18 @@ class RunMonkeyPageComponent extends AuthComponent { } }); + this.authFetch('/api/remote-monkey?action=list_aws') + .then(res => res.json()) + .then(res =>{ + let is_aws = res['is_aws']; + if (is_aws) { + let instances = res['instances']; + if (instances) { + this.setState({isOnAws: true, awsMachines: instances}); + } + } + }); + this.authFetch('/api/client-monkey') .then(res => res.json()) .then(res => { @@ -134,6 +151,56 @@ class RunMonkeyPageComponent extends AuthComponent { }); }; + toggleAws = () => { + this.setState({ + showAws: !this.state.showAws + }); + }; + + runOnAws = () => { + this.setState({ + awsClicked: true + }); + + let instances = this.awsTable.state.selection.map(x => this.instanceIdToInstance(x)); + + this.authFetch('/api/remote-monkey', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({type: 'aws', instances: instances, island_ip: this.state.selectedIp}) + }).then(res => res.json()) + .then(res => { + let result = res['result']; + + // update existing state, not run-over + let prevRes = this.awsTable.state.result; + for (let key in result) { + if (result.hasOwnProperty(key)) { + prevRes[key] = result[key]; + } + } + this.awsTable.setState({ + result: prevRes, + selection: [], + selectAll: false + }); + + this.setState({ + awsClicked: false + }); + }); + }; + + instanceIdToInstance = (instance_id) => { + let instance = this.state.awsMachines.find( + function (inst) { + return inst['instance_id'] === instance_id; + }); + return {'instance_id': instance_id, 'os': instance['os']} + + }; + render() { return (
OR
-+
@@ -196,6 +263,51 @@ class RunMonkeyPageComponent extends AuthComponent { {this.generateCmdDiv()} + { + this.state.isOnAws ? +
+ OR +
+ : + null + } + { + this.state.isOnAws ? ++ +
+ : + null + } ++ Select server IP address +
+ { + this.state.ips.length > 1 ? + + : + } + +Go ahead and monitor the ongoing infection in the Infection Map view. diff --git a/monkey/monkey_island/cc/ui/src/components/run-monkey/AwsRunTable.js b/monkey/monkey_island/cc/ui/src/components/run-monkey/AwsRunTable.js new file mode 100644 index 000000000..6a8fe9416 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/run-monkey/AwsRunTable.js @@ -0,0 +1,109 @@ +import React from 'react'; +import ReactTable from 'react-table' +import checkboxHOC from "react-table/lib/hoc/selectTable"; + +const CheckboxTable = checkboxHOC(ReactTable); + +const columns = [ + { + Header: 'Machines', + columns: [ + { Header: 'Machine', accessor: 'name'}, + { Header: 'Instance ID', accessor: 'instance_id'}, + { Header: 'IP Address', accessor: 'ip_address'}, + { Header: 'OS', accessor: 'os'} + ] + } +]; + +const pageSize = 10; + +class AwsRunTableComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + selection: [], + selectAll: false, + result: {} + } + } + + toggleSelection = (key, shift, row) => { + // start off with the existing state + let selection = [...this.state.selection]; + const keyIndex = selection.indexOf(key); + // check to see if the key exists + if (keyIndex >= 0) { + // it does exist so we will remove it using destructing + selection = [ + ...selection.slice(0, keyIndex), + ...selection.slice(keyIndex + 1) + ]; + } else { + // it does not exist so add it + selection.push(key); + } + // update the state + this.setState({ selection }); + }; + + isSelected = key => { + return this.state.selection.includes(key); + }; + + toggleAll = () => { + const selectAll = !this.state.selectAll; + const selection = []; + if (selectAll) { + // we need to get at the internals of ReactTable + const wrappedInstance = this.checkboxTable.getWrappedInstance(); + // the 'sortedData' property contains the currently accessible records based on the filter and sort + const currentRecords = wrappedInstance.getResolvedState().sortedData; + // we just push all the IDs onto the selection array + currentRecords.forEach(item => { + selection.push(item._original.instance_id); + }); + } + this.setState({ selectAll, selection }); + }; + + getTrProps = (s, r) => { + let color = "inherit"; + if (r) { + let instId = r.original.instance_id; + if (this.isSelected(instId)) { + color = "#ffed9f"; + } else if (this.state.result.hasOwnProperty(instId)) { + color = this.state.result[instId] ? "#00f01b" : '#f00000' + } + } + + return { + style: {backgroundColor: color} + }; + }; + + render() { + return ( +