Most feature is ready

This commit is contained in:
Itay Mizeretz 2019-02-04 18:07:36 +02:00
parent 4e8fe0ec3f
commit 0f4bb7f5f1
8 changed files with 378 additions and 27 deletions

View File

@ -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']
]

View File

@ -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'))

View File

@ -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']

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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;