forked from p15670423/monkey
Merge pull request #1785 from guardicore/1611-interruptable-exploiters
1611 interruptable exploiters
This commit is contained in:
commit
33f2bac275
|
@ -81,7 +81,7 @@ class Configuration(object):
|
||||||
# monkey config
|
# monkey config
|
||||||
###########################
|
###########################
|
||||||
# sets whether or not the monkey is alive. if false will stop scanning and exploiting
|
# sets whether or not the monkey is alive. if false will stop scanning and exploiting
|
||||||
alive = True
|
should_stop = False
|
||||||
|
|
||||||
# depth of propagation
|
# depth of propagation
|
||||||
depth = 2
|
depth = 2
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"inaccessible_subnets": [],
|
"inaccessible_subnets": [],
|
||||||
"blocked_ips": [],
|
"blocked_ips": [],
|
||||||
"current_server": "192.0.2.0:5000",
|
"current_server": "192.0.2.0:5000",
|
||||||
"alive": true,
|
"should_stop": false,
|
||||||
"collect_system_info": true,
|
"collect_system_info": true,
|
||||||
"should_use_mimikatz": true,
|
"should_use_mimikatz": true,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
@ -66,12 +67,14 @@ class HostExploiter:
|
||||||
telemetry_messenger: ITelemetryMessenger,
|
telemetry_messenger: ITelemetryMessenger,
|
||||||
agent_repository: IAgentRepository,
|
agent_repository: IAgentRepository,
|
||||||
options: Dict,
|
options: Dict,
|
||||||
|
interrupt: threading.Event,
|
||||||
):
|
):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.current_depth = current_depth
|
self.current_depth = current_depth
|
||||||
self.telemetry_messenger = telemetry_messenger
|
self.telemetry_messenger = telemetry_messenger
|
||||||
self.agent_repository = agent_repository
|
self.agent_repository = agent_repository
|
||||||
self.options = options
|
self.options = options
|
||||||
|
self.interrupt = interrupt
|
||||||
|
|
||||||
self.pre_exploit()
|
self.pre_exploit()
|
||||||
try:
|
try:
|
||||||
|
@ -91,6 +94,15 @@ class HostExploiter:
|
||||||
)
|
)
|
||||||
self.set_start_time()
|
self.set_start_time()
|
||||||
|
|
||||||
|
def is_interrupted(self):
|
||||||
|
# This method should be refactored to raise an exception to reduce duplication in the
|
||||||
|
# "if is_interrupted: return self.exploitation_results"
|
||||||
|
# Ideally the user should only do "check_for_interrupt()"
|
||||||
|
if self.interrupt.is_set():
|
||||||
|
logger.info("Exploiter has been interrupted")
|
||||||
|
self.exploit_result.error_message = "Exploiter has been interrupted"
|
||||||
|
return self.interrupt.is_set()
|
||||||
|
|
||||||
def post_exploit(self):
|
def post_exploit(self):
|
||||||
self.set_finish_time()
|
self.set_finish_time()
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import threading
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
from infection_monkey.model import VictimHost
|
from infection_monkey.model import VictimHost
|
||||||
|
@ -26,10 +27,17 @@ class ExploiterWrapper:
|
||||||
self._telemetry_messenger = telemetry_messenger
|
self._telemetry_messenger = telemetry_messenger
|
||||||
self._agent_repository = agent_repository
|
self._agent_repository = agent_repository
|
||||||
|
|
||||||
def exploit_host(self, host: VictimHost, current_depth: int, options: Dict):
|
def exploit_host(
|
||||||
|
self, host: VictimHost, current_depth: int, options: Dict, interrupt: threading.Event
|
||||||
|
):
|
||||||
exploiter = self._exploit_class()
|
exploiter = self._exploit_class()
|
||||||
return exploiter.exploit_host(
|
return exploiter.exploit_host(
|
||||||
host, current_depth, self._telemetry_messenger, self._agent_repository, options
|
host,
|
||||||
|
current_depth,
|
||||||
|
self._telemetry_messenger,
|
||||||
|
self._agent_repository,
|
||||||
|
options,
|
||||||
|
interrupt,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
|
@ -49,6 +49,7 @@ class WmiTools(object):
|
||||||
if not domain:
|
if not domain:
|
||||||
domain = host.ip_addr
|
domain = host.ip_addr
|
||||||
|
|
||||||
|
# Impacket has a hard-coded timeout of 30 seconds
|
||||||
dcom = DCOMConnection(
|
dcom = DCOMConnection(
|
||||||
host.ip_addr,
|
host.ip_addr,
|
||||||
username=username,
|
username=username,
|
||||||
|
|
|
@ -15,6 +15,7 @@ from infection_monkey.utils.brute_force import (
|
||||||
get_credential_string,
|
get_credential_string,
|
||||||
)
|
)
|
||||||
from infection_monkey.utils.commands import build_monkey_commandline
|
from infection_monkey.utils.commands import build_monkey_commandline
|
||||||
|
from infection_monkey.utils.threading import interruptable_iter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -28,8 +29,15 @@ class WmiExploiter(HostExploiter):
|
||||||
def _exploit_host(self) -> ExploiterResultData:
|
def _exploit_host(self) -> ExploiterResultData:
|
||||||
|
|
||||||
creds = generate_brute_force_combinations(self.options["credentials"])
|
creds = generate_brute_force_combinations(self.options["credentials"])
|
||||||
|
intp_creds = interruptable_iter(
|
||||||
|
creds,
|
||||||
|
self.interrupt,
|
||||||
|
"WMI exploiter has been interrupted",
|
||||||
|
logging.INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
for user, password, lm_hash, ntlm_hash in intp_creds:
|
||||||
|
|
||||||
for user, password, lm_hash, ntlm_hash in creds:
|
|
||||||
creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash])
|
creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash])
|
||||||
logger.debug(f"Attempting to connect to {self.host} using WMI with {creds_for_log}")
|
logger.debug(f"Attempting to connect to {self.host} using WMI with {creds_for_log}")
|
||||||
|
|
||||||
|
@ -60,21 +68,11 @@ class WmiExploiter(HostExploiter):
|
||||||
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
|
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
|
||||||
self.exploit_result.exploitation_success = True
|
self.exploit_result.exploitation_success = True
|
||||||
|
|
||||||
# query process list and check if monkey already running on victim
|
|
||||||
process_list = WmiTools.list_object(
|
|
||||||
wmi_connection,
|
|
||||||
"Win32_Process",
|
|
||||||
fields=("Caption",),
|
|
||||||
where=f"Name='{ntpath.split(self.options['dropper_target_path_win_64'])[-1]}'",
|
|
||||||
)
|
|
||||||
if process_list:
|
|
||||||
wmi_connection.close()
|
|
||||||
|
|
||||||
logger.debug("Skipping %r - already infected", self.host)
|
|
||||||
return self.exploit_result
|
|
||||||
|
|
||||||
downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"])
|
downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"])
|
||||||
|
|
||||||
|
if self.is_interrupted():
|
||||||
|
return self.exploit_result
|
||||||
|
|
||||||
remote_full_path = SmbTools.copy_file(
|
remote_full_path = SmbTools.copy_file(
|
||||||
self.host,
|
self.host,
|
||||||
downloaded_agent,
|
downloaded_agent,
|
||||||
|
@ -120,7 +118,7 @@ class WmiExploiter(HostExploiter):
|
||||||
self.add_vuln_port(port="unknown")
|
self.add_vuln_port(port="unknown")
|
||||||
self.exploit_result.propagation_success = True
|
self.exploit_result.propagation_success = True
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
error_message = (
|
||||||
"Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, "
|
"Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, "
|
||||||
"cmdline=%r)",
|
"cmdline=%r)",
|
||||||
remote_full_path,
|
remote_full_path,
|
||||||
|
@ -129,6 +127,8 @@ class WmiExploiter(HostExploiter):
|
||||||
result.ReturnValue,
|
result.ReturnValue,
|
||||||
cmdline,
|
cmdline,
|
||||||
)
|
)
|
||||||
|
logger.debug(error_message)
|
||||||
|
self.exploit_result.error_message = error_message
|
||||||
|
|
||||||
result.RemRelease()
|
result.RemRelease()
|
||||||
wmi_connection.close()
|
wmi_connection.close()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from infection_monkey.i_puppet import PluginType, UnknownPluginError
|
from infection_monkey.i_puppet import PluginType, UnknownPluginError
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ class PluginRegistry:
|
||||||
|
|
||||||
logger.debug(f"Plugin '{plugin_name}' loaded")
|
logger.debug(f"Plugin '{plugin_name}' loaded")
|
||||||
|
|
||||||
def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> object:
|
def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> Any:
|
||||||
try:
|
try:
|
||||||
plugin = self._registry[plugin_type][plugin_name]
|
plugin = self._registry[plugin_type][plugin_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -66,7 +66,7 @@ class Puppet(IPuppet):
|
||||||
interrupt: threading.Event,
|
interrupt: threading.Event,
|
||||||
) -> ExploiterResultData:
|
) -> ExploiterResultData:
|
||||||
exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER)
|
exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER)
|
||||||
return exploiter.exploit_host(host, current_depth, options)
|
return exploiter.exploit_host(host, current_depth, options, interrupt)
|
||||||
|
|
||||||
def run_payload(self, name: str, options: Dict, interrupt: threading.Event):
|
def run_payload(self, name: str, options: Dict, interrupt: threading.Event):
|
||||||
payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD)
|
payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD)
|
||||||
|
|
|
@ -8,6 +8,6 @@ class Config(EmbeddedDocument):
|
||||||
See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist
|
See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alive = BooleanField()
|
should_stop = BooleanField()
|
||||||
meta = {"strict": False}
|
meta = {"strict": False}
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -19,11 +19,10 @@ INTERNAL = {
|
||||||
"title": "Monkey",
|
"title": "Monkey",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"alive": {
|
"should_stop": {
|
||||||
"title": "Alive",
|
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": True,
|
"default": False,
|
||||||
"description": "Is the monkey alive",
|
"description": "Was stop command issued for this monkey",
|
||||||
},
|
},
|
||||||
"aws_keys": {
|
"aws_keys": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def set_stop_all(time: float):
|
def set_stop_all(time: float):
|
||||||
for monkey in Monkey.objects():
|
for monkey in Monkey.objects():
|
||||||
monkey.config.alive = False
|
monkey.config.should_stop = True
|
||||||
monkey.save()
|
monkey.save()
|
||||||
agent_controls = AgentControls.objects.first()
|
agent_controls = AgentControls.objects.first()
|
||||||
agent_controls.last_stop_all = time
|
agent_controls.last_stop_all = time
|
||||||
|
@ -25,11 +25,16 @@ def set_stop_all(time: float):
|
||||||
|
|
||||||
def should_agent_die(guid: int) -> bool:
|
def should_agent_die(guid: int) -> bool:
|
||||||
monkey = Monkey.objects(guid=str(guid)).first()
|
monkey = Monkey.objects(guid=str(guid)).first()
|
||||||
return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually(monkey)
|
return _should_agent_stop(monkey) or _is_monkey_killed_manually(monkey)
|
||||||
|
|
||||||
|
|
||||||
def _is_monkey_marked_dead(monkey: Monkey) -> bool:
|
def _should_agent_stop(monkey: Monkey) -> bool:
|
||||||
return not monkey.config.alive
|
if monkey.config.should_stop:
|
||||||
|
# Only stop the agent once, to allow further runs on that machine
|
||||||
|
monkey.config.should_stop = False
|
||||||
|
monkey.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_monkey_killed_manually(monkey: Monkey) -> bool:
|
def _is_monkey_killed_manually(monkey: Monkey) -> bool:
|
||||||
|
|
|
@ -249,7 +249,7 @@ class NodeService:
|
||||||
|
|
||||||
# Cancel the force kill once monkey died
|
# Cancel the force kill once monkey died
|
||||||
if is_dead:
|
if is_dead:
|
||||||
props_to_set["config.alive"] = True
|
props_to_set["config.should_stop"] = False
|
||||||
|
|
||||||
mongo.db.monkey.update({"guid": monkey["guid"]}, {"$set": props_to_set}, upsert=False)
|
mongo.db.monkey.update({"guid": monkey["guid"]}, {"$set": props_to_set}, upsert=False)
|
||||||
|
|
||||||
|
|
|
@ -88,10 +88,11 @@ class MapPageComponent extends AuthComponent {
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({kill_time: Date.now()})
|
// Python uses floating point seconds, Date.now uses milliseconds, so convert
|
||||||
|
body: JSON.stringify({kill_time: Date.now() / 1000.0})
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(res => {this.setState({killPressed: true}); console.log(res)});
|
.then(res => {this.setState({killPressed: true})});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderKillDialogModal = () => {
|
renderKillDialogModal = () => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import threading
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ def powershell_arguments():
|
||||||
"current_depth": 2,
|
"current_depth": 2,
|
||||||
"telemetry_messenger": MagicMock(),
|
"telemetry_messenger": MagicMock(),
|
||||||
"agent_repository": mock_agent_repository,
|
"agent_repository": mock_agent_repository,
|
||||||
|
"interrupt": threading.Event(),
|
||||||
}
|
}
|
||||||
return arguments
|
return arguments
|
||||||
|
|
||||||
|
|
|
@ -10,21 +10,21 @@ from monkey_island.cc.services.infection_lifecycle import should_agent_die
|
||||||
@pytest.mark.usefixtures("uses_database")
|
@pytest.mark.usefixtures("uses_database")
|
||||||
def test_should_agent_die_by_config(monkeypatch):
|
def test_should_agent_die_by_config(monkeypatch):
|
||||||
monkey = Monkey(guid=str(uuid.uuid4()))
|
monkey = Monkey(guid=str(uuid.uuid4()))
|
||||||
monkey.config = Config(alive=False)
|
monkey.config = Config(should_stop=True)
|
||||||
monkey.save()
|
monkey.save()
|
||||||
assert should_agent_die(monkey.guid)
|
assert should_agent_die(monkey.guid)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"monkey_island.cc.services.infection_lifecycle._is_monkey_killed_manually", lambda _: False
|
"monkey_island.cc.services.infection_lifecycle._is_monkey_killed_manually", lambda _: False
|
||||||
)
|
)
|
||||||
monkey.config.alive = True
|
monkey.config.should_stop = True
|
||||||
monkey.save()
|
monkey.save()
|
||||||
assert not should_agent_die(monkey.guid)
|
assert not should_agent_die(monkey.guid)
|
||||||
|
|
||||||
|
|
||||||
def create_monkey(launch_time):
|
def create_monkey(launch_time):
|
||||||
monkey = Monkey(guid=str(uuid.uuid4()))
|
monkey = Monkey(guid=str(uuid.uuid4()))
|
||||||
monkey.config = Config(alive=True)
|
monkey.config = Config(should_stop=False)
|
||||||
monkey.launch_time = launch_time
|
monkey.launch_time = launch_time
|
||||||
monkey.save()
|
monkey.save()
|
||||||
return monkey
|
return monkey
|
||||||
|
|
Loading…
Reference in New Issue