Changes that allow to avoid monkey exploitation redundancy: checking if island can see vulnerable port, checking if monkey was started on island and comparing depth vs maximum depth

This commit is contained in:
VakarisZ 2020-05-20 10:00:42 +03:00
parent 698a13960e
commit 0e54b78664
25 changed files with 214 additions and 50 deletions

View File

@ -8,9 +8,6 @@ from itertools import product
__author__ = 'itamar'
from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException
from infection_monkey.network import info
GUID = str(uuid.getnode())
EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin')
@ -25,14 +22,17 @@ class Configuration(object):
for key, value in list(formatted_data.items()):
if key.startswith('_'):
continue
if key in ["name", "id", "current_server"]:
if key in ["name", "id", "current_server", "max_depth"]:
continue
if self._depth_from_commandline and key == "depth":
self.max_depth = value
continue
if hasattr(self, key):
setattr(self, key, value)
else:
unknown_items.append(key)
if not self.max_depth:
self.max_depth = self.depth
return unknown_items
def from_json(self, json_data):
@ -138,6 +138,8 @@ class Configuration(object):
# depth of propagation
depth = 2
max_depth = None
started_on_island = False
current_server = ""
# Configuration servers to try to connect to, in this order.
@ -235,6 +237,18 @@ class Configuration(object):
cred_list.append(cred)
return cred_list
@staticmethod
def hash_sensitive_data(sensitive_data):
"""
Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is
saved on client machines plain-text.
:param sensitive_data: the data to hash.
:return: the hashed data.
"""
password_hashed = hashlib.sha512(sensitive_data.encode()).hexdigest()
return password_hashed
exploit_user_list = ['Administrator', 'root', 'user']
exploit_password_list = ["Password1!", "1234", "password", "12345678"]
exploit_lm_hash_list = []
@ -262,30 +276,22 @@ class Configuration(object):
extract_azure_creds = True
###########################
# post breach actions
###########################
post_breach_actions = []
custom_PBA_linux_cmd = ""
custom_PBA_windows_cmd = ""
PBA_linux_filename = None
PBA_windows_filename = None
@staticmethod
def hash_sensitive_data(sensitive_data):
"""
Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is
saved on client machines plain-text.
###########################
# testing configuration
###########################
export_monkey_telems = False
:param sensitive_data: the data to hash.
:return: the hashed data.
"""
password_hashed = hashlib.sha512(sensitive_data.encode()).hexdigest()
return password_hashed
@staticmethod
def should_monkey_run():
local_ips = info.local_ips()
if set(local_ips).intersection(set(WormConfiguration.blocked_ips)):
raise PlannedShutdownException("Monkey shouldn't run on current machine "
"(blocked ip or redundant exploitation).")
def get_hop_count(self):
return self.max_depth - self.depth
WormConfiguration = Configuration()

View File

@ -15,6 +15,8 @@ from infection_monkey.transport.tcp import TcpProxy
__author__ = 'hoffer'
from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException
requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__)
@ -321,3 +323,28 @@ class ControlClient(object):
proxies=ControlClient.proxies)
except requests.exceptions.RequestException:
return False
@staticmethod
def should_monkey_run(port: str) -> bool:
if WormConfiguration.get_hop_count() > 1 and \
ControlClient.can_island_see_port(port) and \
WormConfiguration.started_on_island:
raise PlannedShutdownException("Monkey shouldn't run on current machine "
"(it will be exploited later with more depth).")
return True
@staticmethod
def can_island_see_port(port):
try:
url = f"https://{WormConfiguration.current_server}/api/monkey_control/check_remote_port/{port}"
response = requests.get(url, verify=False)
response = json.loads(response.content.decode())
return response['status'] == "port_visible"
except requests.exceptions.RequestException:
return False
@staticmethod
def report_start_on_island():
requests.post(f"https://{WormConfiguration.current_server}/api/monkey_control/started_on_island",
data=json.dumps({'started_on_island': True}),
verify=False)

View File

@ -44,6 +44,7 @@ class MonkeyDrops(object):
arg_parser.add_argument('-s', '--server')
arg_parser.add_argument('-d', '--depth', type=int)
arg_parser.add_argument('-l', '--location')
arg_parser.add_argument('-vp', '--vulnerable-port')
self.monkey_args = args[1:]
self.opts, _ = arg_parser.parse_known_args(args)
@ -115,7 +116,11 @@ class MonkeyDrops(object):
LOG.warning("Cannot set reference date to destination file")
monkey_options = \
build_monkey_commandline_explicitly(self.opts.parent, self.opts.tunnel, self.opts.server, self.opts.depth)
build_monkey_commandline_explicitly(self.opts.parent,
self.opts.tunnel,
self.opts.server,
self.opts.depth,
self.opts.vulnerable_port)
if OperatingSystem.Windows == SystemInfoCollector.get_os():
monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options

View File

@ -73,7 +73,8 @@ class HadoopExploiter(WebRCE):
def build_command(self, path, http_path):
# Build command to execute
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1)
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1,
vulnerable_port=HTTPTools.get_port_from_url(http_path))
if 'linux' in self.host.os['type']:
base_command = HADOOP_LINUX_COMMAND
else:

View File

@ -133,6 +133,7 @@ class MSSQLExploiter(HostExploiter):
# Form monkey's launch command
monkey_args = build_monkey_commandline(self.host,
get_monkey_depth() - 1,
MSSQLExploiter.SQL_DEFAULT_TCP_PORT,
dst_path)
suffix = ">>{}".format(self.payload_file_path)
prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX

View File

@ -329,7 +329,10 @@ class SambaCryExploiter(HostExploiter):
return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_64), "rb")
def get_monkey_commandline_file(self, location):
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, get_monkey_depth() - 1, str(location)))
return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host,
get_monkey_depth() - 1,
SambaCryExploiter.SAMBA_PORT,
str(location)))
@staticmethod
def is_share_writable(smb_client, share):

View File

@ -144,7 +144,11 @@ class ShellShockExploiter(HostExploiter):
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1, dropper_target_path_linux) + ' & '
cmdline += build_monkey_commandline(self.host,
get_monkey_depth() - 1,
HTTPTools.get_port_from_url(url),
dropper_target_path_linux)
cmdline += ' & '
run_path = exploit + cmdline
self.attack_page(url, header, run_path)

View File

@ -28,6 +28,7 @@ class SmbExploiter(HostExploiter):
def __init__(self, host):
super(SmbExploiter, self).__init__(host)
self.vulnerable_port = None
def is_os_supported(self):
if super(SmbExploiter, self).is_os_supported():
@ -36,11 +37,13 @@ class SmbExploiter(HostExploiter):
if not self.host.os.get('type'):
is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445)
if is_smb_open:
self.vulnerable_port = 445
smb_finger = SMBFinger()
smb_finger.get_host_fingerprint(self.host)
else:
is_nb_open, _ = check_tcp_port(self.host.ip_addr, 139)
if is_nb_open:
self.vulnerable_port = 139
self.host.os['type'] = 'windows'
return self.host.os.get('type') in self._TARGET_OS_TYPE
return False
@ -103,10 +106,13 @@ class SmbExploiter(HostExploiter):
if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1,
self.vulnerable_port,
self._config.dropper_target_path_win_32)
else:
cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
build_monkey_commandline(self.host,
get_monkey_depth() - 1,
vulnerable_port=self.vulnerable_port)
smb_conn = False
for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values():

View File

@ -179,7 +179,9 @@ class SSHExploiter(HostExploiter):
try:
cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG)
cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1)
cmdline += build_monkey_commandline(self.host,
get_monkey_depth() - 1,
vulnerable_port=SSH_PORT)
cmdline += " > /dev/null 2>&1 &"
ssh.exec_command(cmdline)

View File

@ -41,7 +41,8 @@ def get_target_monkey_by_os(is_windows, is_32bit):
return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None,
vulnerable_port=None):
cmdline = ""
if parent is not None:
@ -53,17 +54,19 @@ def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, d
if depth is not None:
if depth < 0:
depth = 0
cmdline += " -d %d" % depth
cmdline += f" -d {depth}"
if location is not None:
cmdline += " -l %s" % location
cmdline += f" -l {location}"
if vulnerable_port is not None:
cmdline += f" -vp {str(vulnerable_port)}"
return cmdline
def build_monkey_commandline(target_host, depth, location=None):
def build_monkey_commandline(target_host, depth, vulnerable_port, location=None):
from infection_monkey.config import GUID
return build_monkey_commandline_explicitly(
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
GUID, target_host.default_tunnel, target_host.default_server, depth, location, vulnerable_port)
def get_monkey_depth():

View File

@ -73,6 +73,10 @@ class HTTPTools(object):
lock.acquire()
return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd
@staticmethod
def get_port_from_url(url: str) -> int:
return urllib.parse.urlparse(url).port
class MonkeyHTTPServer(HTTPTools):
def __init__(self, host):

View File

@ -132,7 +132,9 @@ class VSFTPDExploiter(HostExploiter):
T1222Telem(ScanStatus.USED, change_permission.decode(), self.host).send()
# Run monkey on the machine
parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1)
parameters = build_monkey_commandline(self.host,
get_monkey_depth() - 1,
vulnerable_port=FTP_PORT)
run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters}
# Set unlimited to memory

View File

@ -42,6 +42,8 @@ class WebRCE(HostExploiter):
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
self.skip_exist = self._config.skip_exploit_if_file_exist
self.vulnerable_urls = []
self.target_url = None
self.vulnerable_port = None
def get_exploit_config(self):
"""
@ -87,27 +89,30 @@ class WebRCE(HostExploiter):
if not self.vulnerable_urls:
return False
self.target_url = self.vulnerable_urls[0]
self.vulnerable_port = HTTPTools.get_port_from_url(self.target_url)
# Skip if monkey already exists and this option is given
if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(self.vulnerable_urls[0]):
if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(self.target_url):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
# Check for targets architecture (if it's 32 or 64 bit)
if not exploit_config['blind_exploit'] and not self.set_host_arch(self.vulnerable_urls[0]):
if not exploit_config['blind_exploit'] and not self.set_host_arch(self.target_url):
return False
# Upload the right monkey to target
data = self.upload_monkey(self.vulnerable_urls[0], exploit_config['upload_commands'])
data = self.upload_monkey(self.target_url, exploit_config['upload_commands'])
if data is False:
return False
# Change permissions to transform monkey into executable file
if self.change_permissions(self.vulnerable_urls[0], data['path']) is False:
if self.change_permissions(self.target_url, data['path']) is False:
return False
# Execute remote monkey
if self.execute_remote_monkey(self.vulnerable_urls[0], data['path'], exploit_config['dropper']) is False:
if self.execute_remote_monkey(self.target_url, data['path'], exploit_config['dropper']) is False:
return False
return True
@ -403,10 +408,15 @@ class WebRCE(HostExploiter):
default_path = self.get_default_dropper_path()
if default_path is False:
return False
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path)
monkey_cmd = build_monkey_commandline(self.host,
get_monkey_depth() - 1,
self.vulnerable_port,
default_path)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd}
else:
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1)
monkey_cmd = build_monkey_commandline(self.host,
get_monkey_depth() - 1,
self.vulnerable_port)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd}
try:
LOG.info("Trying to execute monkey using command: {}".format(command))
@ -489,3 +499,6 @@ class WebRCE(HostExploiter):
except KeyError:
LOG.debug("Target's machine type was not set. Using win-32 dropper path.")
return self._config.dropper_target_path_win_32
def set_vulnerable_port_from_url(self, url):
self.vulnerable_port = HTTPTools.get_port_from_url(url)

View File

@ -234,11 +234,15 @@ class Ms08_067_Exploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1,
build_monkey_commandline(self.host,
get_monkey_depth() - 1,
SRVSVC_Exploit.TELNET_PORT,
self._config.dropper_target_path_win_32)
else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
build_monkey_commandline(self.host,
get_monkey_depth() - 1,
vulnerable_port=SRVSVC_Exploit.TELNET_PORT)
try:
sock.send("start %s\r\n" % (cmdline,))

View File

@ -21,6 +21,7 @@ class WmiExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)'
VULNERABLE_PORT = 135
def __init__(self, host):
super(WmiExploiter, self).__init__(host)
@ -94,11 +95,15 @@ class WmiExploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
build_monkey_commandline(
self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
build_monkey_commandline(self.host,
get_monkey_depth() - 1,
WmiExploiter.VULNERABLE_PORT,
self._config.dropper_target_path_win_32)
else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
build_monkey_commandline(self.host,
get_monkey_depth() - 1,
WmiExploiter.VULNERABLE_PORT)
# execute the remote monkey
result = WmiTools.get_object(wmi_connection, "Win32_Process").Create(cmdline,

View File

@ -27,7 +27,7 @@ from infection_monkey.telemetry.trace_telem import TraceTelem
from infection_monkey.telemetry.tunnel_telem import TunnelTelem
from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach
from infection_monkey.network.tools import get_interface_to_target
from infection_monkey.network.tools import get_interface_to_target, is_running_on_server
from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError, FailedExploitationError
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
from common.utils.attack_utils import ScanStatus, UsageEnum
@ -71,6 +71,7 @@ class InfectionMonkey(object):
arg_parser.add_argument('-t', '--tunnel')
arg_parser.add_argument('-s', '--server')
arg_parser.add_argument('-d', '--depth', type=int)
arg_parser.add_argument('-vp', '--vulnerable-port')
self._opts, self._args = arg_parser.parse_known_args(self._args)
self._parent = self._opts.parent
@ -116,6 +117,10 @@ class InfectionMonkey(object):
self.shutdown_by_not_alive_config()
if self.is_started_on_island():
ControlClient.report_start_on_island()
ControlClient.should_monkey_run(self._opts.vulnerable_port)
if firewall.is_enabled():
firewall.add_firewall_rule()
@ -126,8 +131,6 @@ class InfectionMonkey(object):
StateTelem(is_done=False, version=get_version()).send()
TunnelTelem().send()
WormConfiguration.should_monkey_run()
LOG.debug("Starting the post-breach phase.")
self.collect_system_info_if_configured()
PostBreach().execute_all_configured()
@ -379,3 +382,6 @@ class InfectionMonkey(object):
raise PlannedShutdownException("Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel))
self._default_server = WormConfiguration.current_server
LOG.debug("default server set to: %s" % self._default_server)
def is_started_on_island(self):
return is_running_on_server(self._default_server) and WormConfiguration.depth == WormConfiguration.max_depth

View File

@ -7,7 +7,7 @@ import struct
import time
import re
from infection_monkey.network.info import get_routes
from infection_monkey.network.info import get_routes, local_ips
from infection_monkey.pyinstaller_utils import get_binary_file_path
from infection_monkey.utils.environment import is_64bit_python
@ -309,3 +309,7 @@ def get_interface_to_target(dst):
paths.sort()
ret = paths[-1][1]
return ret[1]
def is_running_on_server(ip: str) -> bool:
return ip in local_ips()

View File

@ -16,10 +16,12 @@ from monkey_island.cc.resources.island_logs import IslandLog
from monkey_island.cc.resources.monkey import Monkey
from monkey_island.cc.resources.monkey_configuration import MonkeyConfiguration
from monkey_island.cc.resources.island_configuration import IslandConfiguration
from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland
from monkey_island.cc.resources.monkey_download import MonkeyDownload
from monkey_island.cc.resources.netmap import NetMap
from monkey_island.cc.resources.node import Node
from monkey_island.cc.resources.node_states import NodeStates
from monkey_island.cc.resources.monkey_control.remote_port_check import RemotePortCheck
from monkey_island.cc.resources.remote_run import RemoteRun
from monkey_island.cc.resources.reporting.report import Report
from monkey_island.cc.resources.root import Root
@ -121,6 +123,8 @@ def init_api_resources(api):
api.add_resource(AttackConfiguration, '/api/attack')
api.add_resource(AttackReport, '/api/attack/report')
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')
api.add_resource(RemotePortCheck, '/api/monkey_control/check_remote_port/<string:port>')
api.add_resource(StartedOnIsland, '/api/monkey_control/started_on_island')
api.add_resource(MonkeyTest, '/api/test/monkey')
api.add_resource(ClearCaches, '/api/test/clear_caches')

View File

@ -0,0 +1,14 @@
import flask_restful
from flask import request
from monkey_island.cc.services.remote_port_check import check_tcp_port
class RemotePortCheck(flask_restful.Resource):
# Used by monkey. can't secure.
def get(self, port):
if check_tcp_port(request.remote_addr, port):
return {"status": "port_visible"}
else:
return {"status": "port_invisible"}

View File

@ -0,0 +1,16 @@
import json
import flask_restful
from flask import request, make_response
from monkey_island.cc.services.config import ConfigService
class StartedOnIsland(flask_restful.Resource):
# Used by monkey. can't secure.
def post(self):
data = json.loads(request.data)
if data['started_on_island']:
ConfigService.set_started_on_island(True)
return make_response({}, 200)

View File

@ -321,3 +321,7 @@ class ConfigService:
@staticmethod
def add_blocked_ip(ip_):
ConfigService.append_to_config_array(['basic_network', 'general', 'blocked_ips'], ip_)
@staticmethod
def set_started_on_island(value: bool):
ConfigService.set_config_value(['internal', 'general', 'started_on_island'], value)

View File

@ -564,6 +564,13 @@ SCHEMA = {
"default": r"monkey_dir",
"description": "Directory name for the directory which will contain all of the monkey files"
},
"started_on_island": {
"title": "Started on island",
"type": "boolean",
"default": False,
"description": "Was exploitation started from island"
"(did monkey with max depth ran on island)"
},
}
},
"classes": {

View File

@ -0,0 +1,19 @@
import socket
DEFAULT_TIMEOUT = 5 # Seconds
def check_tcp_port(ip: str, port: str, timeout=DEFAULT_TIMEOUT) -> bool:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((ip, int(port)))
except socket.timeout:
return False
except socket.error:
return False
finally:
sock.close()
return True

View File

@ -69,7 +69,11 @@ class ConfigurePageComponent extends AuthComponent {
cnc: {},
network: {},
exploits: {},
internal: {}
internal: {
general: {
started_on_island: {'ui:widget': 'hidden'}
}
}
})
}