Merge pull request #1785 from guardicore/1611-interruptable-exploiters

1611 interruptable exploiters
This commit is contained in:
Mike Salvatore 2022-03-18 08:28:52 -04:00 committed by GitHub
commit 33f2bac275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 65 additions and 36 deletions

View File

@ -81,7 +81,7 @@ class Configuration(object):
# monkey config
###########################
# sets whether or not the monkey is alive. if false will stop scanning and exploiting
alive = True
should_stop = False
# depth of propagation
depth = 2

View File

@ -9,7 +9,7 @@
"inaccessible_subnets": [],
"blocked_ips": [],
"current_server": "192.0.2.0:5000",
"alive": true,
"should_stop": false,
"collect_system_info": true,
"should_use_mimikatz": true,
"depth": 2,

View File

@ -1,4 +1,5 @@
import logging
import threading
from abc import abstractmethod
from datetime import datetime
from typing import Dict
@ -66,12 +67,14 @@ class HostExploiter:
telemetry_messenger: ITelemetryMessenger,
agent_repository: IAgentRepository,
options: Dict,
interrupt: threading.Event,
):
self.host = host
self.current_depth = current_depth
self.telemetry_messenger = telemetry_messenger
self.agent_repository = agent_repository
self.options = options
self.interrupt = interrupt
self.pre_exploit()
try:
@ -91,6 +94,15 @@ class HostExploiter:
)
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):
self.set_finish_time()

View File

@ -1,3 +1,4 @@
import threading
from typing import Dict, Type
from infection_monkey.model import VictimHost
@ -26,10 +27,17 @@ class ExploiterWrapper:
self._telemetry_messenger = telemetry_messenger
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()
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__(

View File

@ -49,6 +49,7 @@ class WmiTools(object):
if not domain:
domain = host.ip_addr
# Impacket has a hard-coded timeout of 30 seconds
dcom = DCOMConnection(
host.ip_addr,
username=username,

View File

@ -15,6 +15,7 @@ from infection_monkey.utils.brute_force import (
get_credential_string,
)
from infection_monkey.utils.commands import build_monkey_commandline
from infection_monkey.utils.threading import interruptable_iter
logger = logging.getLogger(__name__)
@ -28,8 +29,15 @@ class WmiExploiter(HostExploiter):
def _exploit_host(self) -> ExploiterResultData:
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])
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.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"])
if self.is_interrupted():
return self.exploit_result
remote_full_path = SmbTools.copy_file(
self.host,
downloaded_agent,
@ -120,7 +118,7 @@ class WmiExploiter(HostExploiter):
self.add_vuln_port(port="unknown")
self.exploit_result.propagation_success = True
else:
logger.debug(
error_message = (
"Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, "
"cmdline=%r)",
remote_full_path,
@ -129,6 +127,8 @@ class WmiExploiter(HostExploiter):
result.ReturnValue,
cmdline,
)
logger.debug(error_message)
self.exploit_result.error_message = error_message
result.RemRelease()
wmi_connection.close()

View File

@ -1,4 +1,5 @@
import logging
from typing import Any
from infection_monkey.i_puppet import PluginType, UnknownPluginError
@ -27,7 +28,7 @@ class PluginRegistry:
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:
plugin = self._registry[plugin_type][plugin_name]
except KeyError:

View File

@ -66,7 +66,7 @@ class Puppet(IPuppet):
interrupt: threading.Event,
) -> ExploiterResultData:
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):
payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD)

View File

@ -8,6 +8,6 @@ class Config(EmbeddedDocument):
See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist
"""
alive = BooleanField()
should_stop = BooleanField()
meta = {"strict": False}
pass

View File

@ -19,11 +19,10 @@ INTERNAL = {
"title": "Monkey",
"type": "object",
"properties": {
"alive": {
"title": "Alive",
"should_stop": {
"type": "boolean",
"default": True,
"description": "Is the monkey alive",
"default": False,
"description": "Was stop command issued for this monkey",
},
"aws_keys": {
"type": "object",

View File

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
def set_stop_all(time: float):
for monkey in Monkey.objects():
monkey.config.alive = False
monkey.config.should_stop = True
monkey.save()
agent_controls = AgentControls.objects.first()
agent_controls.last_stop_all = time
@ -25,11 +25,16 @@ def set_stop_all(time: float):
def should_agent_die(guid: int) -> bool:
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:
return not monkey.config.alive
def _should_agent_stop(monkey: Monkey) -> bool:
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:

View File

@ -249,7 +249,7 @@ class NodeService:
# Cancel the force kill once monkey died
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)

View File

@ -88,10 +88,11 @@ class MapPageComponent extends AuthComponent {
{
method: 'POST',
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 => {this.setState({killPressed: true}); console.log(res)});
.then(res => {this.setState({killPressed: true})});
};
renderKillDialogModal = () => {

View File

@ -1,3 +1,4 @@
import threading
from io import BytesIO
from unittest.mock import MagicMock
@ -43,6 +44,7 @@ def powershell_arguments():
"current_depth": 2,
"telemetry_messenger": MagicMock(),
"agent_repository": mock_agent_repository,
"interrupt": threading.Event(),
}
return arguments

View File

@ -10,21 +10,21 @@ from monkey_island.cc.services.infection_lifecycle import should_agent_die
@pytest.mark.usefixtures("uses_database")
def test_should_agent_die_by_config(monkeypatch):
monkey = Monkey(guid=str(uuid.uuid4()))
monkey.config = Config(alive=False)
monkey.config = Config(should_stop=True)
monkey.save()
assert should_agent_die(monkey.guid)
monkeypatch.setattr(
"monkey_island.cc.services.infection_lifecycle._is_monkey_killed_manually", lambda _: False
)
monkey.config.alive = True
monkey.config.should_stop = True
monkey.save()
assert not should_agent_die(monkey.guid)
def create_monkey(launch_time):
monkey = Monkey(guid=str(uuid.uuid4()))
monkey.config = Config(alive=True)
monkey.config = Config(should_stop=False)
monkey.launch_time = launch_time
monkey.save()
return monkey