Merge pull request #87 from guardicore/feature/send-raw-log

Feature/send raw log
This commit is contained in:
Daniel Goldberg 2018-03-06 18:10:22 +02:00 committed by GitHub
commit 8f5643b0b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 227 additions and 30 deletions

View File

@ -106,6 +106,7 @@ class Configuration(object):
dropper_log_path_linux = '/tmp/user-1562' dropper_log_path_linux = '/tmp/user-1562'
monkey_log_path_windows = '%temp%\\~df1563.tmp' monkey_log_path_windows = '%temp%\\~df1563.tmp'
monkey_log_path_linux = '/tmp/user-1563' monkey_log_path_linux = '/tmp/user-1563'
send_log_to_server = True
########################### ###########################
# dropper config # dropper config

View File

@ -111,6 +111,21 @@ class ControlClient(object):
LOG.warn("Error connecting to control server %s: %s", LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc) WormConfiguration.current_server, exc)
@staticmethod
def send_log(log):
if not WormConfiguration.current_server:
return
try:
telemetry = {'monkey_guid': GUID, 'log': json.dumps(log)}
reply = requests.post("https://%s/api/log" % (WormConfiguration.current_server,),
data=json.dumps(telemetry),
headers={'content-type': 'application/json'},
verify=False,
proxies=ControlClient.proxies)
except Exception as exc:
LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc)
@staticmethod @staticmethod
def load_control_config(): def load_control_config():
if not WormConfiguration.current_server: if not WormConfiguration.current_server:

View File

@ -48,6 +48,7 @@
"max_iterations": 3, "max_iterations": 3,
"monkey_log_path_windows": "%temp%\\~df1563.tmp", "monkey_log_path_windows": "%temp%\\~df1563.tmp",
"monkey_log_path_linux": "/tmp/user-1563", "monkey_log_path_linux": "/tmp/user-1563",
"send_log_to_server": true,
"ms08_067_exploit_attempts": 5, "ms08_067_exploit_attempts": 5,
"ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT", "ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT",
"ms08_067_remote_user_pass": "Password1!", "ms08_067_remote_user_pass": "Password1!",

View File

@ -12,6 +12,7 @@ from config import WormConfiguration, EXTERNAL_CONFIG_FILE
from dropper import MonkeyDrops from dropper import MonkeyDrops
from model import MONKEY_ARG, DROPPER_ARG from model import MONKEY_ARG, DROPPER_ARG
from monkey import InfectionMonkey from monkey import InfectionMonkey
import utils
if __name__ == "__main__": if __name__ == "__main__":
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@ -78,12 +79,10 @@ def main():
try: try:
if MONKEY_ARG == monkey_mode: if MONKEY_ARG == monkey_mode:
log_path = os.path.expandvars( log_path = utils.get_monkey_log_path()
WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" else WormConfiguration.monkey_log_path_linux
monkey_cls = InfectionMonkey monkey_cls = InfectionMonkey
elif DROPPER_ARG == monkey_mode: elif DROPPER_ARG == monkey_mode:
log_path = os.path.expandvars( log_path = utils.get_dropper_log_path()
WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" else WormConfiguration.dropper_log_path_linux
monkey_cls = MonkeyDrops monkey_cls = MonkeyDrops
else: else:
return True return True
@ -91,6 +90,8 @@ def main():
return True return True
if WormConfiguration.use_file_logging: if WormConfiguration.use_file_logging:
if os.path.exists(log_path):
os.remove(log_path)
LOG_CONFIG['handlers']['file']['filename'] = log_path LOG_CONFIG['handlers']['file']['filename'] = log_path
LOG_CONFIG['root']['handlers'].append('file') LOG_CONFIG['root']['handlers'].append('file')
else: else:
@ -120,6 +121,8 @@ def main():
json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': ')) json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': '))
return True return True
except Exception:
LOG.exception("Exception thrown from monkey's start function")
finally: finally:
monkey.cleanup() monkey.cleanup()

View File

@ -6,6 +6,7 @@ import sys
import time import time
import tunnel import tunnel
import utils
from config import WormConfiguration from config import WormConfiguration
from control import ControlClient from control import ControlClient
from model import DELAY_DELETE_CMD from model import DELAY_DELETE_CMD
@ -226,6 +227,9 @@ class InfectionMonkey(object):
firewall.close() firewall.close()
if WormConfiguration.send_log_to_server:
self.send_log()
self._singleton.unlock() self._singleton.unlock()
if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find('python'): if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find('python'):
@ -244,3 +248,13 @@ class InfectionMonkey(object):
LOG.error("Exception in self delete: %s", exc) LOG.error("Exception in self delete: %s", exc)
LOG.info("Monkey is shutting down") LOG.info("Monkey is shutting down")
def send_log(self):
monkey_log_path = utils.get_monkey_log_path()
if os.path.exists(monkey_log_path):
with open(monkey_log_path, 'r') as f:
log = f.read()
else:
log = ''
ControlClient.send_log(log)

14
infection_monkey/utils.py Normal file
View File

@ -0,0 +1,14 @@
import os
import sys
from config import WormConfiguration
def get_monkey_log_path():
return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \
else WormConfiguration.monkey_log_path_linux
def get_dropper_log_path():
return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
else WormConfiguration.dropper_log_path_linux

View File

@ -8,11 +8,12 @@ from flask import Flask, send_from_directory, make_response
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from cc.auth import init_jwt from cc.auth import init_jwt
from cc.database import mongo from cc.database import mongo, database
from cc.environment.environment import env from cc.environment.environment import env
from cc.resources.client_run import ClientRun from cc.resources.client_run import ClientRun
from cc.resources.edge import Edge from cc.resources.edge import Edge
from cc.resources.local_run import LocalRun from cc.resources.local_run import LocalRun
from cc.resources.log import Log
from cc.resources.monkey import Monkey from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration from cc.resources.monkey_configuration import MonkeyConfiguration
from cc.resources.monkey_download import MonkeyDownload from cc.resources.monkey_download import MonkeyDownload
@ -83,6 +84,7 @@ def init_app(mongo_url):
mongo.init_app(app) mongo.init_app(app)
with app.app_context(): with app.app_context():
database.init()
ConfigService.init_config() ConfigService.init_config()
app.add_url_rule('/', 'serve_home', serve_home) app.add_url_rule('/', 'serve_home', serve_home)
@ -101,5 +103,6 @@ def init_app(mongo_url):
api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/')
api.add_resource(Report, '/api/report', '/api/report/') api.add_resource(Report, '/api/report', '/api/report/')
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/')
return app return app

View File

@ -1,5 +1,5 @@
from flask_pymongo import PyMongo import gridfs
from flask_pymongo import MongoClient from flask_pymongo import MongoClient, PyMongo
from pymongo.errors import ServerSelectionTimeoutError from pymongo.errors import ServerSelectionTimeoutError
__author__ = 'Barak' __author__ = 'Barak'
@ -7,6 +7,17 @@ __author__ = 'Barak'
mongo = PyMongo() mongo = PyMongo()
class Database:
def __init__(self):
self.gridfs = None
def init(self):
self.gridfs = gridfs.GridFS(mongo.db)
database = Database()
def is_db_server_up(mongo_url): def is_db_server_up(mongo_url):
client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
try: try:

View File

@ -0,0 +1,34 @@
import json
import flask_restful
from bson import ObjectId
from flask import request
from cc.auth import jwt_required
from cc.database import mongo
from cc.services.log import LogService
from cc.services.node import NodeService
__author__ = "itay.mizeretz"
class Log(flask_restful.Resource):
@jwt_required()
def get(self):
monkey_id = request.args.get('id')
exists_monkey_id = request.args.get('exists')
if monkey_id:
return LogService.get_log_by_monkey_id(ObjectId(monkey_id))
else:
return LogService.log_exists(ObjectId(exists_monkey_id))
# Used by monkey. can't secure.
def post(self):
telemetry_json = json.loads(request.data)
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id']
# This shouldn't contain any unicode characters. this'll take 2 time less space.
log_data = str(telemetry_json['log'])
log_id = LogService.add_log(monkey_id, log_data)
return mongo.db.log.find_one_or_404({"_id": log_id})

View File

@ -36,7 +36,7 @@ class Root(flask_restful.Resource):
@staticmethod @staticmethod
def reset_db(): def reset_db():
[mongo.db[x].drop() for x in ['config', 'monkey', 'telemetry', 'node', 'edge', 'report']] [mongo.db[x].drop() for x in mongo.db.collection_names()]
ConfigService.init_config() ConfigService.init_config()
return jsonify(status='OK') return jsonify(status='OK')

View File

@ -483,6 +483,12 @@ SCHEMA = {
"type": "string", "type": "string",
"default": "%temp%\\~df1563.tmp", "default": "%temp%\\~df1563.tmp",
"description": "The fullpath of the monkey log file on Windows" "description": "The fullpath of the monkey log file on Windows"
},
"send_log_to_server": {
"title": "Send log to server",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey sends its log to the Monkey Island server"
} }
} }
}, },

View File

@ -0,0 +1,48 @@
from datetime import datetime
import cc.services.node
from cc.database import mongo, database
__author__ = "itay.mizeretz"
class LogService:
def __init__(self):
pass
@staticmethod
def get_log_by_monkey_id(monkey_id):
log = mongo.db.log.find_one({'monkey_id': monkey_id})
if log:
log_file = database.gridfs.get(log['file_id'])
monkey_label = cc.services.node.NodeService.get_monkey_label(
cc.services.node.NodeService.get_monkey_by_id(log['monkey_id']))
return \
{
'monkey_label': monkey_label,
'log': log_file.read(),
'timestamp': log['timestamp']
}
@staticmethod
def remove_logs_by_monkey_id(monkey_id):
log = mongo.db.log.find_one({'monkey_id': monkey_id})
if log is not None:
database.gridfs.delete(log['file_id'])
mongo.db.log.delete_one({'monkey_id': monkey_id})
@staticmethod
def add_log(monkey_id, log_data, timestamp=datetime.now()):
LogService.remove_logs_by_monkey_id(monkey_id)
file_id = database.gridfs.put(log_data)
return mongo.db.log.insert(
{
'monkey_id': monkey_id,
'file_id': file_id,
'timestamp': timestamp
}
)
@staticmethod
def log_exists(monkey_id):
return mongo.db.log.find_one({'monkey_id': monkey_id}) is not None

View File

@ -1,9 +1,12 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from bson import ObjectId from bson import ObjectId
import cc.services.log
from cc.database import mongo from cc.database import mongo
from cc.services.edge import EdgeService from cc.services.edge import EdgeService
from cc.utils import local_ip_addresses from cc.utils import local_ip_addresses
__author__ = "itay.mizeretz" __author__ = "itay.mizeretz"
@ -54,6 +57,7 @@ class NodeService:
else: else:
new_node["services"] = [] new_node["services"] = []
new_node['has_log'] = cc.services.log.LogService.log_exists(ObjectId(node_id))
return new_node return new_node
@staticmethod @staticmethod
@ -241,7 +245,7 @@ class NodeService:
@staticmethod @staticmethod
def get_monkey_island_pseudo_net_node(): def get_monkey_island_pseudo_net_node():
return\ return \
{ {
"id": NodeService.get_monkey_island_pseudo_id(), "id": NodeService.get_monkey_island_pseudo_id(),
"label": "MonkeyIsland", "label": "MonkeyIsland",

View File

@ -63,6 +63,7 @@
"dependencies": { "dependencies": {
"bootstrap": "^3.3.7", "bootstrap": "^3.3.7",
"core-js": "^2.5.1", "core-js": "^2.5.1",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0", "fetch": "^1.1.0",
"js-file-download": "^0.4.1", "js-file-download": "^0.4.1",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",

View File

@ -2,6 +2,7 @@ import React from 'react';
import {Icon} from 'react-fa'; import {Icon} from 'react-fa';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap'; import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import download from 'downloadjs'
import AuthComponent from '../../AuthComponent'; import AuthComponent from '../../AuthComponent';
class PreviewPaneComponent extends AuthComponent { class PreviewPaneComponent extends AuthComponent {
@ -82,16 +83,56 @@ class PreviewPaneComponent extends AuthComponent {
</th> </th>
<td> <td>
<Toggle id={asset.id} checked={!asset.config.alive} icons={false} disabled={asset.dead} <Toggle id={asset.id} checked={!asset.config.alive} icons={false} disabled={asset.dead}
onChange={(e) => this.forceKill(e, asset)} /> onChange={(e) => this.forceKill(e, asset)}/>
</td> </td>
</tr> </tr>
); );
} }
unescapeLog(st) {
return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string.
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\b/g, "\b")
.replace(/\\f/g, "\f")
.replace(/\\"/g, '\"')
.replace(/\\'/g, "\'")
.replace(/\\&/g, "\&");
}
downloadLog(asset) {
fetch('/api/log?id=' + asset.id)
.then(res => res.json())
.then(res => {
let timestamp = res['timestamp'];
timestamp = timestamp.substr(0, timestamp.indexOf('.'));
let filename = res['monkey_label'].split(':').join('-') + ' - ' + timestamp + '.log';
let logContent = this.unescapeLog(res['log']);
download(logContent, filename, 'text/plain');
});
}
downloadLogRow(asset) {
return (
<tr>
<th>
Download Log
</th>
<td>
<a type="button" className="btn btn-primary"
disabled={!asset.has_log}
onClick={() => this.downloadLog(asset)}>Download</a>
</td>
</tr>
);
}
exploitsTimeline(asset) { exploitsTimeline(asset) {
if (asset.exploits.length === 0) { if (asset.exploits.length === 0) {
return (<div />); return (<div/>);
} }
return ( return (
@ -101,9 +142,9 @@ class PreviewPaneComponent extends AuthComponent {
{this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')} {this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')}
</h4> </h4>
<ul className="timeline"> <ul className="timeline">
{ asset.exploits.map(exploit => {asset.exploits.map(exploit =>
<li key={exploit.timestamp}> <li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} /> <div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div> <div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div> <div>{exploit.origin}</div>
<div>{exploit.exploiter}</div> <div>{exploit.exploiter}</div>
@ -141,6 +182,7 @@ class PreviewPaneComponent extends AuthComponent {
{this.servicesRow(asset)} {this.servicesRow(asset)}
{this.accessibleRow(asset)} {this.accessibleRow(asset)}
{this.forceKillRow(asset)} {this.forceKillRow(asset)}
{this.downloadLogRow(asset)}
</tbody> </tbody>
</table> </table>
{this.exploitsTimeline(asset)} {this.exploitsTimeline(asset)}
@ -173,9 +215,9 @@ class PreviewPaneComponent extends AuthComponent {
<div> <div>
<h4 style={{'marginTop': '2em'}}>Timeline</h4> <h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline"> <ul className="timeline">
{ edge.exploits.map(exploit => {edge.exploits.map(exploit =>
<li key={exploit.timestamp}> <li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} /> <div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div> <div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div> <div>{exploit.origin}</div>
<div>{exploit.exploiter}</div> <div>{exploit.exploiter}</div>
@ -221,9 +263,9 @@ class PreviewPaneComponent extends AuthComponent {
return ( return (
<div className="preview-pane"> <div className="preview-pane">
{ !info ? {!info ?
<span> <span>
<Icon name="hand-o-left" style={{'marginRight': '0.5em'}} /> <Icon name="hand-o-left" style={{'marginRight': '0.5em'}}/>
Select an item on the map for a detailed look Select an item on the map for a detailed look
</span> </span>
: :