Merge pull request #650 from guardicore/feature/exploitation_redundancy_fix

Redundant exploitations fix
This commit is contained in:
VakarisZ 2020-05-25 18:35:10 +03:00 committed by GitHub
commit ed276895a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 449 additions and 164 deletions

View File

@ -10,7 +10,13 @@ In order to execute the entire test suite, you must know the external IP of the
this information in the GCP Console `Compute Engine/VM Instances` under _External IP_.
#### Running in command line
Run the following command:
Blackbox tests have following parameters:
- `--island=IP` Sets island's IP
- `--no-gcp` (Optional) Use for no interaction with the cloud (local test).
- `--quick-performance-tests` (Optional) If enabled performance tests won't reset island and won't send telemetries,
instead will just test performance of endpoints in already present island state.
Example run command:
`monkey\envs\monkey_zoo\blackbox>python -m pytest -s --island=35.207.152.72:5000 test_blackbox.py`

View File

@ -4,8 +4,23 @@ import pytest
def pytest_addoption(parser):
parser.addoption("--island", action="store", default="",
help="Specify the Monkey Island address (host+port).")
parser.addoption("--no-gcp", action="store_true", default=False,
help="Use for no interaction with the cloud.")
parser.addoption("--quick-performance-tests", action="store_true", default=False,
help="If enabled performance tests won't reset island and won't send telemetries, "
"instead will just test performance of already present island state.")
@pytest.fixture(scope='module')
@pytest.fixture(scope='session')
def island(request):
return request.config.getoption("--island")
@pytest.fixture(scope='session')
def no_gcp(request):
return request.config.getoption("--no-gcp")
@pytest.fixture(scope='session')
def quick_performance_tests(request):
return request.config.getoption("--quick-performance-tests")

View File

@ -92,7 +92,8 @@ class MonkeyIslandRequests(object):
return requests.patch(self.addr + url, # noqa: DUO123
data=data,
headers=self.get_jwt_header(),
verify=False)
verify=False
)
@_Decorators.refresh_jwt_token
def delete(self, url):

View File

@ -2,14 +2,15 @@
"basic": {
"credentials": {
"exploit_password_list": [
"Password1!",
"12345678",
"^NgDvY59~8"
"Xk8VDTsC",
"^NgDvY59~8",
"Ivrrw5zEzs",
"3Q=(Ge(+&w]*",
"`))jU7L(w}",
"t67TC5ZDmz"
],
"exploit_user_list": [
"Administrator",
"m0nk3y",
"user"
"m0nk3y"
]
},
"general": {
@ -23,11 +24,38 @@
"local_network_scan": false,
"subnet_scan_list": [
"10.2.2.2",
"10.2.2.4"
"10.2.2.3",
"10.2.2.4",
"10.2.2.5",
"10.2.2.8",
"10.2.2.9",
"10.2.1.10",
"10.2.0.11",
"10.2.0.12",
"10.2.2.11",
"10.2.2.12",
"10.2.2.14",
"10.2.2.15",
"10.2.2.16",
"10.2.2.18",
"10.2.2.19",
"10.2.2.20",
"10.2.2.21",
"10.2.2.23",
"10.2.2.24"
]
},
"network_analysis": {
"inaccessible_subnets": []
"inaccessible_subnets": [
"10.2.2.0/30",
"10.2.2.8/30",
"10.2.2.24/32",
"10.2.2.23/32",
"10.2.2.21/32",
"10.2.2.19/32",
"10.2.2.18/32",
"10.2.2.17/32"
]
}
},
"cnc": {
@ -45,10 +73,17 @@
"exploits": {
"general": {
"exploiter_classes": [
"SmbExploiter",
"WmiExploiter",
"SSHExploiter",
"MSSQLExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
"HadoopExploiter"
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter",
"VSFTPDExploiter",
"MSSQLExploiter"
],
"skip_exploit_if_file_exist": false
},
@ -57,9 +92,6 @@
"remote_user_pass": "Password1!",
"user_to_add": "Monkey_IUSER_SUPPORT"
},
"rdp_grinder": {
"rdp_use_vbs_download": true
},
"sambacry": {
"sambacry_folder_paths_to_guess": [
"/",
@ -109,7 +141,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 1,
"keep_tunnel_open_time": 60,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -123,6 +155,9 @@
"monkey_log_path_linux": "/tmp/user-1563",
"monkey_log_path_windows": "%temp%\\~df1563.tmp",
"send_log_to_server": true
},
"testing": {
"export_monkey_telems": true
}
},
"monkey": {
@ -137,24 +172,32 @@
},
"general": {
"alive": true,
"post_breach_actions": []
"post_breach_actions": [
"CommunicateAsNewUser"
]
},
"life_cycle": {
"max_iterations": 1,
"retry_failed_explotation": true,
"timeout_between_iterations": 100,
"victims_max_exploit": 7,
"victims_max_find": 30
"victims_max_exploit": 15,
"victims_max_find": 100
},
"system_info": {
"collect_system_info": true,
"extract_azure_creds": false,
"should_use_mimikatz": true
"extract_azure_creds": true,
"should_use_mimikatz": true,
"system_info_collectors_classes": [
"EnvironmentCollector",
"AwsCollector",
"HostnameCollector",
"ProcessListCollector"
]
}
},
"network": {
"ping_scanner": {
"ping_scan_timeout": 500
"ping_scan_timeout": 1000
},
"tcp_scanner": {
"HTTP_PORTS": [
@ -166,7 +209,7 @@
],
"tcp_scan_get_banner": true,
"tcp_scan_interval": 0,
"tcp_scan_timeout": 1000,
"tcp_scan_timeout": 3000,
"tcp_target_ports": [
22,
2222,
@ -179,7 +222,8 @@
8008,
3306,
9200,
7001
7001,
8088
]
}
}

View File

@ -27,7 +27,8 @@ LOGGER = logging.getLogger(__name__)
@pytest.fixture(autouse=True, scope='session')
def GCPHandler(request):
def GCPHandler(request, no_gcp):
if not no_gcp:
GCPHandler = gcp_machine_handlers.GCPHandler()
GCPHandler.start_machines(" ".join(GCP_TEST_MACHINE_LIST))
wait_machine_bootup()
@ -49,8 +50,9 @@ def wait_machine_bootup():
@pytest.fixture(scope='class')
def island_client(island):
def island_client(island, quick_performance_tests):
island_client_object = MonkeyIslandClient(island)
if not quick_performance_tests:
island_client_object.reset_env()
yield island_client_object
@ -130,7 +132,7 @@ class TestMonkeyBlackbox(object):
def test_wmi_pth(self, island_client):
TestMonkeyBlackbox.run_exploitation_test(island_client, "WMI_PTH.conf", "WMI_PTH")
def test_report_generation_performance(self, island_client):
def test_report_generation_performance(self, island_client, quick_performance_tests):
"""
This test includes the SSH + Elastic + Hadoop + MSSQL machines all in one test
for a total of 8 machines including the Monkey Island.
@ -138,22 +140,30 @@ class TestMonkeyBlackbox(object):
Is has 2 analyzers - the regular one which checks all the Monkeys
and the Timing one which checks how long the report took to execute
"""
if not quick_performance_tests:
TestMonkeyBlackbox.run_performance_test(ReportGenerationTest,
island_client,
"PERFORMANCE.conf",
timeout_in_seconds=10*60)
else:
LOGGER.error("This test doesn't support 'quick_performance_tests' option.")
assert False
def test_map_generation_performance(self, island_client):
def test_map_generation_performance(self, island_client, quick_performance_tests):
if not quick_performance_tests:
TestMonkeyBlackbox.run_performance_test(MapGenerationTest,
island_client,
"PERFORMANCE.conf",
timeout_in_seconds=10*60)
else:
LOGGER.error("This test doesn't support 'quick_performance_tests' option.")
assert False
def test_report_generation_from_fake_telemetries(self, island_client):
ReportGenerationFromTelemetryTest(island_client).run()
def test_report_generation_from_fake_telemetries(self, island_client, quick_performance_tests):
ReportGenerationFromTelemetryTest(island_client, quick_performance_tests).run()
def test_map_generation_from_fake_telemetries(self, island_client):
MapGenerationFromTelemetryTest(island_client).run()
def test_map_generation_from_fake_telemetries(self, island_client, quick_performance_tests):
MapGenerationFromTelemetryTest(island_client, quick_performance_tests).run()
def test_telem_performance(self, island_client):
TelemetryPerformanceTest(island_client).test_telemetry_performance()
def test_telem_performance(self, island_client, quick_performance_tests):
TelemetryPerformanceTest(island_client, quick_performance_tests).test_telemetry_performance()

View File

@ -17,9 +17,6 @@ class EndpointPerformanceTest(BasicTest):
self.island_client = island_client
def run(self) -> bool:
if not self.island_client.is_all_monkeys_dead():
raise RuntimeError("Can't test report times since not all Monkeys have died.")
# Collect timings for all pages
self.island_client.clear_caches()
endpoint_timings = {}

View File

@ -17,7 +17,7 @@ class MapGenerationFromTelemetryTest(PerformanceTest):
TEST_NAME = "Map generation from fake telemetries test"
def __init__(self, island_client, break_on_timeout=False):
def __init__(self, island_client, quick_performance_test: bool, break_on_timeout=False):
self.island_client = island_client
performance_config = PerformanceTestConfig(max_allowed_single_page_time=MAX_ALLOWED_SINGLE_PAGE_TIME,
max_allowed_total_time=MAX_ALLOWED_TOTAL_TIME,
@ -25,7 +25,8 @@ class MapGenerationFromTelemetryTest(PerformanceTest):
break_on_timeout=break_on_timeout)
self.performance_test_workflow = TelemetryPerformanceTestWorkflow(MapGenerationFromTelemetryTest.TEST_NAME,
self.island_client,
performance_config)
performance_config,
quick_performance_test)
def run(self):
self.performance_test_workflow.run()

View File

@ -23,6 +23,8 @@ class PerformanceTestWorkflow(BasicTest):
self.island_client.kill_all_monkeys()
self.exploitation_test.wait_until_monkeys_die()
self.exploitation_test.wait_for_monkey_process_to_finish()
if not self.island_client.is_all_monkeys_dead():
raise RuntimeError("Can't test report times since not all Monkeys have died.")
performance_test = EndpointPerformanceTest(self.name, self.performance_config, self.island_client)
try:
if not self.island_client.is_all_monkeys_dead():

View File

@ -21,7 +21,7 @@ class ReportGenerationFromTelemetryTest(PerformanceTest):
TEST_NAME = "Map generation from fake telemetries test"
def __init__(self, island_client, break_on_timeout=False):
def __init__(self, island_client, quick_performance_test, break_on_timeout=False):
self.island_client = island_client
performance_config = PerformanceTestConfig(max_allowed_single_page_time=MAX_ALLOWED_SINGLE_PAGE_TIME,
max_allowed_total_time=MAX_ALLOWED_TOTAL_TIME,
@ -29,7 +29,8 @@ class ReportGenerationFromTelemetryTest(PerformanceTest):
break_on_timeout=break_on_timeout)
self.performance_test_workflow = TelemetryPerformanceTestWorkflow(ReportGenerationFromTelemetryTest.TEST_NAME,
self.island_client,
performance_config)
performance_config,
quick_performance_test)
def run(self):
self.performance_test_workflow.run()

View File

@ -10,9 +10,9 @@ class FakeMonkey:
self.original_guid = guid
self.fake_ip_generator = fake_ip_generator
self.on_island = on_island
self.fake_guid = str(random.randint(1000000000000, 9999999999999))
self.fake_guid = str(random.randint(1000000000000, 9999999999999)) # noqa: DUO102
self.fake_ips = fake_ip_generator.generate_fake_ips_for_real_ips(ips)
def change_fake_data(self):
self.fake_ips = self.fake_ip_generator.generate_fake_ips_for_real_ips(self.original_ips)
self.fake_guid = str(random.randint(1000000000000, 9999999999999))
self.fake_guid = str(random.randint(1000000000000, 9999999999999)) # noqa: DUO102

View File

@ -18,8 +18,9 @@ MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=60)
class TelemetryPerformanceTest:
def __init__(self, island_client: MonkeyIslandClient):
def __init__(self, island_client: MonkeyIslandClient, quick_performance_test: bool):
self.island_client = island_client
self.quick_performance_test = quick_performance_test
def test_telemetry_performance(self):
LOGGER.info("Starting telemetry performance test.")
@ -36,6 +37,8 @@ class TelemetryPerformanceTest:
telemetry_parse_times[telemetry_endpoint] = self.get_telemetry_time(telemetry)
test_config = PerformanceTestConfig(MAX_ALLOWED_SINGLE_TELEM_PARSE_TIME, MAX_ALLOWED_TOTAL_TIME)
PerformanceAnalyzer(test_config, telemetry_parse_times).analyze_test_results()
if not self.quick_performance_test:
self.island_client.reset_env()
def get_telemetry_time(self, telemetry):
content = telemetry['content']

View File

@ -6,15 +6,18 @@ from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test impor
class TelemetryPerformanceTestWorkflow(BasicTest):
def __init__(self, name, island_client, performance_config: PerformanceTestConfig):
def __init__(self, name, island_client, performance_config: PerformanceTestConfig, quick_performance_test):
self.name = name
self.island_client = island_client
self.performance_config = performance_config
self.quick_performance_test = quick_performance_test
def run(self):
try:
if not self.quick_performance_test:
TelemetryPerformanceTest(island_client=self.island_client).test_telemetry_performance()
performance_test = EndpointPerformanceTest(self.name, self.performance_config, self.island_client)
assert performance_test.run()
finally:
if not self.quick_performance_test:
self.island_client.reset_env()

View File

@ -15,10 +15,10 @@ class GCPHandler(object):
self.zone = zone
try:
# pass the key file to gcp
subprocess.call(GCPHandler.get_auth_command(key_path), shell=True)
subprocess.call(GCPHandler.get_auth_command(key_path), shell=True) # noqa: DUO116
LOGGER.info("GCP Handler passed key")
# set project
subprocess.call(GCPHandler.get_set_project_command(project_id), shell=True)
subprocess.call(GCPHandler.get_set_project_command(project_id), shell=True) # noqa: DUO116
LOGGER.info("GCP Handler set project")
LOGGER.info("GCP Handler initialized successfully")
except Exception as e:
@ -32,14 +32,14 @@ class GCPHandler(object):
"""
LOGGER.info("Setting up all GCP machines...")
try:
subprocess.call((GCPHandler.MACHINE_STARTING_COMMAND % (machine_list, self.zone)), shell=True)
subprocess.call((GCPHandler.MACHINE_STARTING_COMMAND % (machine_list, self.zone)), shell=True) # noqa: DUO116
LOGGER.info("GCP machines successfully started.")
except Exception as e:
LOGGER.error("GCP Handler failed to start GCP machines: %s" % e)
def stop_machines(self, machine_list):
try:
subprocess.call((GCPHandler.MACHINE_STOPPING_COMMAND % (machine_list, self.zone)), shell=True)
subprocess.call((GCPHandler.MACHINE_STOPPING_COMMAND % (machine_list, self.zone)), shell=True) # noqa: DUO116
LOGGER.info("GCP machines stopped successfully.")
except Exception as e:
LOGGER.error("GCP Handler failed to stop network machines: %s" % e)

View File

@ -13,6 +13,7 @@ GUID = str(uuid.getnode())
EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin')
SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list", "exploit_ssh_keys"]
LOCAL_CONFIG_VARS = ["name", "id", "current_server", "max_depth"]
HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden"
@ -22,14 +23,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 LOCAL_CONFIG_VARS:
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):
@ -135,6 +139,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.
@ -232,6 +238,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 = []
@ -259,23 +277,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
def get_hop_distance_to_island(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,29 @@ class ControlClient(object):
proxies=ControlClient.proxies)
except requests.exceptions.RequestException:
return False
@staticmethod
def should_monkey_run(vulnerable_port: str) -> bool:
if vulnerable_port and \
WormConfiguration.get_hop_distance_to_island() > 1 and \
ControlClient.can_island_see_port(vulnerable_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,12 @@ 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(parent=self.opts.parent,
tunnel=self.opts.tunnel,
server=self.opts.server,
depth=self.opts.depth,
location=None,
vulnerable_port=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=HadoopExploiter.HADOOP_PORTS[0][0])
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,29 +41,32 @@ 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:
cmdline += " -p " + parent
cmdline += f" -p {parent}"
if tunnel is not None:
cmdline += " -t " + tunnel
cmdline += f" -t {tunnel}"
if server is not None:
cmdline += " -s " + server
cmdline += f" -s {server}"
if depth is not None:
if depth < 0:
if int(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 {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

@ -0,0 +1,28 @@
import unittest
from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
class TestHelpers(unittest.TestCase):
def test_build_monkey_commandline_explicitly(self):
test1 = " -p 101010 -t 10.10.101.10 -s 127.127.127.127:5000 -d 0 -l C:\\windows\\abc -vp 80"
result1 = build_monkey_commandline_explicitly(101010,
"10.10.101.10",
"127.127.127.127:5000",
0,
"C:\\windows\\abc",
80)
test2 = " -p parent -s 127.127.127.127:5000 -d 0 -vp 80"
result2 = build_monkey_commandline_explicitly(parent="parent",
server="127.127.127.127:5000",
depth="0",
vulnerable_port="80")
self.assertEqual(test1, result1)
self.assertEqual(test2, result2)
if __name__ == '__main__':
unittest.main()

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

@ -10,6 +10,7 @@ from infection_monkey.network.HostFinger import HostFinger
from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir
from infection_monkey.utils.monkey_log_path import get_monkey_log_path
from infection_monkey.utils.environment import is_windows_os
from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException
from infection_monkey.config import WormConfiguration
from infection_monkey.control import ControlClient
from infection_monkey.model import DELAY_DELETE_CMD
@ -26,12 +27,13 @@ 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
from common.version import get_version
from infection_monkey.exploit.HostExploiter import HostExploiter
from monkey_island.cc.network_utils import remove_port_from_ip_string
MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, shutting down"
@ -40,10 +42,6 @@ __author__ = 'itamar'
LOG = logging.getLogger(__name__)
class PlannedShutdownException(Exception):
pass
class InfectionMonkey(object):
def __init__(self, args):
self._keep_running = False
@ -74,7 +72,9 @@ 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.log_arguments()
self._parent = self._opts.parent
self._default_tunnel = self._opts.tunnel
@ -119,6 +119,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()
@ -380,3 +384,11 @@ 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):
island_ip = remove_port_from_ip_string(self._default_server)
return is_running_on_server(island_ip) and WormConfiguration.depth == WormConfiguration.max_depth
def log_arguments(self):
arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()])
LOG.info(f"Monkey started with arguments: {arg_string}")

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

@ -0,0 +1,2 @@
class PlannedShutdownException(Exception):
pass

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

@ -10,7 +10,7 @@ import pymongo
from monkey_island.cc.environment import Environment
# Disable "unverified certificate" warnings when sending requests to island
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # noqa: DUO131
logger = logging.getLogger(__name__)
@ -29,7 +29,9 @@ class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler):
post_data = self.rfile.read(content_length).decode()
island_server_path = BootloaderHTTPRequestHandler.get_bootloader_resource_url(self.request.getsockname()[0])
island_server_path = parse.urljoin(island_server_path, self.path[1:])
r = requests.post(url=island_server_path, data=post_data, verify=False)
# The island server doesn't always have a correct SSL cert installed (By default it comes with a self signed one),
# that's why we're not verifying the cert in this request.
r = requests.post(url=island_server_path, data=post_data, verify=False) # noqa: DUO123
try:
if r.status_code != 200:

View File

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
from monkey_island.cc.app import init_app
from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.network_utils import local_ip_addresses
from monkey_island.cc.environment.environment import env
from monkey_island.cc.database import is_db_server_up, get_db_version
from monkey_island.cc.resources.monkey_download import MonkeyDownload

View File

@ -8,7 +8,7 @@ import ring
from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
from monkey_island.cc.models.command_control_channel import CommandControlChannel
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.network_utils import local_ip_addresses
from common.cloud import environment_names
MAX_MONKEYS_AMOUNT_TO_CACHE = 100

View File

@ -1,12 +1,12 @@
import array
import collections
import ipaddress
import socket
import struct
import sys
from typing import List
import collections
from urllib.parse import urlparse
import array
import struct
import ipaddress
from netifaces import interfaces, ifaddresses, AF_INET
from ring import lru
@ -86,3 +86,8 @@ def get_subnets():
]
)
return subnets
def remove_port_from_ip_string(ip_string: str) -> str:
url = urlparse("http://" + ip_string)
return str(url.hostname)

View File

@ -10,7 +10,7 @@ from monkey_island.cc.environment.environment import env
from monkey_island.cc.models import Monkey
from monkey_island.cc.resources.monkey_download import get_monkey_executable
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.network_utils import local_ip_addresses
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
__author__ = 'Barak'

View File

@ -58,6 +58,7 @@ class Monkey(flask_restful.Resource):
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False)
# Used by monkey. can't secure.
# Called on monkey wakeup to initialize local configuration
@TestTelemStore.store_test_telem
def post(self, **kw):
monkey_json = json.loads(request.data)
@ -74,16 +75,11 @@ class Monkey(flask_restful.Resource):
# if new monkey telem, change config according to "new monkeys" config.
db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})
if not db_monkey:
# we pull it encrypted because we then decrypt it for the monkey in get
# Update monkey configuration
new_config = ConfigService.get_flat_config(False, False)
monkey_json['config'] = monkey_json.get('config', {})
monkey_json['config'].update(new_config)
else:
db_config = db_monkey.get('config', {})
if 'current_server' in db_config:
del db_config['current_server']
monkey_json.get('config', {}).update(db_config)
# try to find new monkey parent
parent = monkey_json.get('parent')

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 port and 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

@ -8,7 +8,7 @@ from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo
from monkey_island.cc.services.database import Database
from monkey_island.cc.services.infection_lifecycle import InfectionLifecycle
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.network_utils import local_ip_addresses
__author__ = 'Barak'

View File

@ -19,6 +19,8 @@ logger = logging.getLogger(__name__)
class TestTelemStore:
TELEMS_EXPORTED = False
@staticmethod
def store_test_telem(f):
@wraps(f)
@ -46,6 +48,7 @@ class TestTelemStore:
for test_telem in TestTelem.objects():
with open(TestTelemStore.get_unique_file_path_for_test_telem(TELEM_SAMPLE_DIR, test_telem), 'w') as file:
file.write(test_telem.to_json(indent=2))
TestTelemStore.TELEMS_EXPORTED = True
logger.info("Telemetries exported!")
@staticmethod

View File

@ -7,7 +7,7 @@ import monkey_island.cc.services.post_breach_files
from monkey_island.cc.database import mongo
from monkey_island.cc.environment.environment import env
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.network_utils import local_ip_addresses
from .config_schema import SCHEMA
from monkey_island.cc.encryptor import encryptor
@ -74,6 +74,12 @@ class ConfigService:
mongo.db.config.update({'name': 'newconfig'},
{"$set": {mongo_key: value}})
@staticmethod
def append_to_config_array(config_key_as_arr, value):
mongo_key = ".".join(config_key_as_arr)
mongo.db.config.update({'name': 'newconfig'},
{"$push": {mongo_key: value}})
@staticmethod
def get_flat_config(is_initial_config=False, should_decrypt=True):
config_json = ConfigService.get_config(is_initial_config, should_decrypt)
@ -311,3 +317,7 @@ class ConfigService:
@staticmethod
def is_test_telem_export_enabled():
return ConfigService.get_config_value(['internal', 'testing', 'export_monkey_telems'])
@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

@ -47,5 +47,5 @@ class InfectionLifecycle:
# we want to skip and reply.
if not is_report_being_generated() and not ReportService.is_latest_report_exists():
safe_generate_reports()
if ConfigService.is_test_telem_export_enabled():
if ConfigService.is_test_telem_export_enabled() and not TestTelemStore.TELEMS_EXPORTED:
TestTelemStore.export_test_telems()

View File

@ -8,7 +8,7 @@ import monkey_island.cc.services.log
from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.utils import local_ip_addresses, is_local_ips
from monkey_island.cc.network_utils import local_ip_addresses, is_local_ips
from monkey_island.cc import models
from monkey_island.cc.services.utils.node_states import NodeStates

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

@ -16,7 +16,7 @@ from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.reporting.pth_report import PTHReportService
from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report
from monkey_island.cc.utils import local_ip_addresses, get_subnets
from monkey_island.cc.network_utils import local_ip_addresses, get_subnets
__author__ = "itay.mizeretz"

View File

@ -1,4 +1,5 @@
import logging
from ipaddress import ip_address
from monkey_island.cc.encryptor import encryptor
from monkey_island.cc.services import mimikatz_utils

View File

@ -30,7 +30,6 @@ class ConfigurePageComponent extends AuthComponent {
lastAction: 'none',
sections: [],
selectedSection: 'attack',
monkeysRan: false,
PBAwinFile: [],
PBAlinuxFile: [],
showAttackAlert: false
@ -70,7 +69,11 @@ class ConfigurePageComponent extends AuthComponent {
cnc: {},
network: {},
exploits: {},
internal: {}
internal: {
general: {
started_on_island: {'ui:widget': 'hidden'}
}
}
})
}
@ -108,7 +111,6 @@ class ConfigurePageComponent extends AuthComponent {
selectedSection: 'attack'
})
});
this.updateMonkeysRunning();
};
updateConfig = () => {
@ -359,14 +361,6 @@ class ConfigurePageComponent extends AuthComponent {
event.target.value = null;
};
updateMonkeysRunning = () => {
this.authFetch('/api')
.then(res => res.json())
.then(res => {
this.setState({monkeysRan: res['completed_steps']['run_monkey']});
});
};
PBAwindows = () => {
return (<FilePond
server={{
@ -464,19 +458,6 @@ class ConfigurePageComponent extends AuthComponent {
</div>)
};
renderConfigWontChangeWarning = () => {
return (<div>
{this.state.monkeysRan ?
<div className="alert alert-warning">
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
Changed configuration will only apply to new infections.
"Start over" to run again with different configuration.
</div>
: ''
}
</div>)
};
renderBasicNetworkWarning = () => {
if (this.state.selectedSection === 'basic_network') {
return (<div className="alert alert-info">
@ -514,7 +495,6 @@ class ConfigurePageComponent extends AuthComponent {
{this.renderAttackAlertModal()}
<h1 className="page-title">Monkey Configuration</h1>
{this.renderNav()}
{this.renderConfigWontChangeWarning()}
{content}
<div className="text-center">
<button type="submit" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>