Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rely only on explicit status #10

Merged
merged 22 commits into from
Nov 20, 2023
102 changes: 40 additions & 62 deletions agent/metasploit_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@
logger = logging.getLogger(__name__)

MODULE_TIMEOUT = 300
WORKSPACE_ARG = "WORKSPACE => Ostorlab"
MSF_SAFE_INDICATOR = "[-]"
MSF_UNKNOWN_INDICATOR = "Cannot reliably check exploitability"
MSF_DEFAULT_AUXILIARY_MESSAGE = """
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
"""
METASPLOIT_AGENT_KEY = b"agent_metasploit_asset"


Expand All @@ -59,8 +52,8 @@ def __init__(
agent_settings: runtime_definitions.AgentSettings,
) -> None:
agent.Agent.__init__(self, agent_definition, agent_settings)
vuln_mixin.AgentReportVulnMixin.__init__(self)
persist_mixin.AgentPersistMixin.__init__(self, agent_settings)
vuln_mixin.AgentReportVulnMixin.__init__(self)
self._config = self.args.get("config", [])
if self._config is None:
raise ValueError("Metasploit module(s) must be specified.")
Expand All @@ -78,7 +71,6 @@ def process(self, message: m.Message) -> None:
return

client = utils.connect_msfrpc()
cid = client.consoles.console().cid
for entry in self._config:
module = entry.get("module")
options = entry.get("options") or []
Expand All @@ -89,62 +81,43 @@ def process(self, message: m.Message) -> None:
logger.error("Specified module %s does not exist", module)
continue
logger.info("Selected metasploit module: %s", selected_module.modulename)
vhost, rport = utils.prepare_target(message)
try:
module_instance = self._set_module_args(
selected_module, vhost, rport, options
)
except ValueError:
logger.error(
"Failed to set arguments for %s", selected_module.modulename
)
continue
if module_instance.moduletype == "exploit":
job = module_instance.check_exploit()
elif module_instance.moduletype == "auxiliary":
job = module_instance.execute()
else:
logger.error(
"%s module type is not implemented", module_instance.moduletype
)
continue
job_uuid = job.get("uuid")
if job_uuid is not None:
results = self._get_job_results(client, job_uuid)
else:
results = None

if isinstance(results, dict) and results.get("code") in ["safe", "unknown"]:
continue

target = (
module_instance.runoptions.get("VHOST")
or module_instance.runoptions.get("RHOSTS")
or module_instance.runoptions.get("DOMAIN")
)
technical_detail = f"Using `{module_instance.moduletype}` module `{module_instance.modulename}`\n"
technical_detail += f"Target: {target}\n"

if isinstance(results, dict) and results.get("code") == "vulnerable":
technical_detail += f'Message: \n```{results["message"]}```'
else:
console_output = client.consoles.console(cid).run_module_with_output(
module_instance
)
targets = utils.prepare_targets(message)
for target in targets:
rhost = target.host
3asm marked this conversation as resolved.
Show resolved Hide resolved
rport = target.port
is_ssl = target.scheme == "https"
try:
module_output = console_output.split(WORKSPACE_ARG)[1]
except IndexError:
logger.error("Unexpected console output:\n %s", console_output)
continue
if MSF_SAFE_INDICATOR in module_output:
module_instance = self._set_module_args(
selected_module, rhost, rport, is_ssl, options
)
except ValueError:
logger.error(
"Failed to set arguments for %s", selected_module.modulename
)
continue
if MSF_UNKNOWN_INDICATOR in module_output:
job = module_instance.check_exploit()
if job.get("error") is True:
logger.error(
"Metasploit Error: %s", job.get("error_string", "Unknown Error")
)
continue
if MSF_DEFAULT_AUXILIARY_MESSAGE == module_output:

job_uuid = job.get("uuid")
if job_uuid is None:
continue
technical_detail += f"Message: \n```{module_output}```"

self._emit_results(module_instance, technical_detail)
results = self._get_job_results(client, job_uuid)

if isinstance(results, dict) and results.get("code") in [
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
"vulnerable",
"appears",
]:
technical_detail = f"Using `{module_instance.moduletype}` module `{module_instance.modulename}`\n"
technical_detail += f"Target: {rhost}:{rport}\n"
technical_detail += f'Message: \n```{results["message"]}```'

self._emit_results(module_instance, technical_detail)

client.logout()

self._mark_target_as_processed(message)
Expand Down Expand Up @@ -247,9 +220,13 @@ def _set_module_args(
selected_module: msfrpc.MsfModule,
vhost: str,
rport: int,
is_ssl: bool,
options: list[dict[str, str]],
) -> msfrpc.MsfModule:
rhost = socket.gethostbyname(vhost)
try:
rhost = socket.gethostbyname(vhost)
except socket.gaierror as exc:
raise ValueError("The specified target is not valid") from exc
if "RHOSTS" in selected_module.required:
selected_module["RHOSTS"] = rhost
elif "DOMAIN" in selected_module.required:
Expand All @@ -262,7 +239,8 @@ def _set_module_args(
selected_module["VHOST"] = vhost
if "RPORT" in selected_module.missing_required:
selected_module["RPORT"] = rport

if "SSL" in selected_module.options:
selected_module["SSL"] = is_ssl
for arg in options:
arg_name = arg["name"]
if arg_name in selected_module.options:
Expand Down
43 changes: 29 additions & 14 deletions agent/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Utilities for agent Metasploit"""
import dataclasses
import ipaddress
from typing import cast
from urllib import parse as urlparser

Expand Down Expand Up @@ -40,12 +41,11 @@ class Target:
port: int


def _get_port(message: m.Message) -> int:
def _get_port(message: m.Message, scheme: str) -> int:
"""Returns the port to be used for the target."""
if message.data.get("port") is not None:
return int(message.data["port"])
else:
return DEFAULT_PORT
if message.data.get("port") is None:
return SCHEME_TO_PORT.get(scheme) or DEFAULT_PORT
return int(message.data["port"])


def _get_scheme(message: m.Message) -> str:
Expand Down Expand Up @@ -77,8 +77,8 @@ def get_unique_check_key(message: m.Message) -> str | None:
if target is not None:
return f"{target.scheme}_{target.host}_{target.port}"
elif message.data.get("name") is not None:
port = _get_port(message)
schema = _get_scheme(message)
port = _get_port(message, schema)
domain = message.data["name"]
return f"{schema}_{domain}_{port}"
return None
Expand All @@ -101,27 +101,42 @@ def _get_target_from_url(message: m.Message) -> Target | None:
and parsed_url.netloc.split(":")[-1] != ""
):
port = int(parsed_url.netloc.split(":")[-1])
args_port = _get_port(message)
args_port = _get_port(message, schema)
port = port or SCHEME_TO_PORT.get(schema) or args_port
target = Target(host=domain_name, scheme=schema, port=port)
return target


def prepare_target(message: m.Message) -> tuple[str, int]:
def prepare_targets(message: m.Message) -> list[Target]:
"""Prepare targets based on type, if a domain name is provided, port and protocol are collected
from the config."""
if (host := message.data.get("host")) is not None:
port = _get_port(message)
return host, port
scheme = _get_scheme(message)
port = _get_port(message, scheme)
try:
mask = int(message.data.get("mask", None))
3asm marked this conversation as resolved.
Show resolved Hide resolved
except ValueError as exc:
raise ValueError("Invalid network mask provided") from exc
if mask is None:
hosts = ipaddress.ip_network(host)
else:
mask = int(mask)
if message.data.get("version") == 4 and mask < 24:
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("Subnet mask below 24 is not supported.")
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
if message.data.get("version") == 6 and mask < 120:
raise ValueError("Subnet mask below 120 is not supported")
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
hosts = ipaddress.ip_network(f"{host}/{mask}", strict=False)
return [Target(host=str(h), port=port, scheme=scheme) for h in hosts]
elif (host := message.data.get("name")) is not None:
port = _get_port(message)
return host, port
scheme = _get_scheme(message)
port = _get_port(message, scheme)
return [Target(host=host, port=port, scheme=scheme)]
elif (url := message.data.get("url")) is not None:
parsed_url = urlparser.urlparse(url)
host = parsed_url.netloc
scheme = parsed_url.scheme
port = SCHEME_TO_PORT.get(scheme) or DEFAULT_PORT
return host, port
port = _get_port(message, scheme)
return [Target(host=host, port=port, scheme=scheme)]
else:
raise NotImplementedError

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def scan_message_ipv6() -> message.Message:
selector = "v3.asset.ip.v6"
msg_data = {
"host": "2001:db8:3333:4444:5555:6666:7777:8888",
"mask": "32",
"mask": "128",
"version": 6,
}
return message.Message.from_data(selector, data=msg_data)
Expand Down
Loading
Loading