forked from p15670423/monkey
Most feature is ready
This commit is contained in:
parent
4e8fe0ec3f
commit
0f4bb7f5f1
|
@ -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']
|
||||
]
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -1,27 +1,36 @@
|
|||
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
|
||||
|
||||
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):
|
||||
|
@ -30,10 +39,18 @@ class AwsCmdRunner(object):
|
|||
time.sleep(0.5)
|
||||
curr_time = time.time()
|
||||
|
||||
return AwsCmdResult(command_info)
|
||||
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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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 (
|
||||
<Col xs={12} lg={8}>
|
||||
|
@ -166,7 +233,7 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
<p className="text-center">
|
||||
OR
|
||||
</p>
|
||||
<p style={{'marginBottom': '2em'}}>
|
||||
<p style={this.state.showManual || !this.state.isOnAws ? {'marginBottom': '2em'} : {}}>
|
||||
<button onClick={this.toggleManual} className={'btn btn-default btn-lg center-block' + (this.state.showManual ? ' active' : '')}>
|
||||
Run on machine of your choice
|
||||
</button>
|
||||
|
@ -196,6 +263,51 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
{this.generateCmdDiv()}
|
||||
</div>
|
||||
</Collapse>
|
||||
{
|
||||
this.state.isOnAws ?
|
||||
<p className="text-center">
|
||||
OR
|
||||
</p>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
this.state.isOnAws ?
|
||||
<p style={{'marginBottom': '2em'}}>
|
||||
<button onClick={this.toggleAws} className={'btn btn-default btn-lg center-block' + (this.state.showAws ? ' active' : '')}>
|
||||
Run on AWS machine of your choice
|
||||
</button>
|
||||
</p>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Collapse in={this.state.showAws}>
|
||||
<div style={{'marginBottom': '2em'}}>
|
||||
<p style={{'fontSize': '1.2em'}}>
|
||||
Select server IP address
|
||||
</p>
|
||||
{
|
||||
this.state.ips.length > 1 ?
|
||||
<Nav bsStyle="pills" justified activeKey={this.state.selectedIp} onSelect={this.setSelectedIp}
|
||||
style={{'marginBottom': '2em'}}>
|
||||
{this.state.ips.map(ip => <NavItem key={ip} eventKey={ip}>{ip}</NavItem>)}
|
||||
</Nav>
|
||||
: <div style={{'marginBottom': '2em'}} />
|
||||
}
|
||||
|
||||
<AwsRunTable
|
||||
data={this.state.awsMachines}
|
||||
ref={r => (this.awsTable = r)}
|
||||
/>
|
||||
<button
|
||||
onClick={this.runOnAws}
|
||||
className={'btn btn-default btn-md center-block'}
|
||||
disabled={this.state.awsClicked}>
|
||||
Run on selected machines
|
||||
{ this.state.awsClicked ? <Icon name="refresh" className="text-success" style={{'marginLeft': '5px'}}/> : null }
|
||||
</button>
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
<p style={{'fontSize': '1.2em'}}>
|
||||
Go ahead and monitor the ongoing infection in the <Link to="/infection/map">Infection Map</Link> view.
|
||||
|
|
|
@ -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 (
|
||||
<div className="data-table-container">
|
||||
<CheckboxTable
|
||||
ref={r => (this.checkboxTable = r)}
|
||||
keyField="instance_id"
|
||||
columns={columns}
|
||||
data={this.props.data}
|
||||
showPagination={true}
|
||||
defaultPageSize={pageSize}
|
||||
className="-highlight"
|
||||
selectType="checkbox"
|
||||
toggleSelection={this.toggleSelection}
|
||||
isSelected={this.isSelected}
|
||||
toggleAll={this.toggleAll}
|
||||
selectAll={this.state.selectAll}
|
||||
getTrProps={this.getTrProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AwsRunTableComponent;
|
Loading…
Reference in New Issue