Skip to content

Commit

Permalink
Merge pull request #10 from Ostorlab/msf_fp
Browse files Browse the repository at this point in the history
Rely only on explicit status
  • Loading branch information
3asm authored Nov 20, 2023
2 parents 24a6749 + 017b16f commit df4bb1a
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 218 deletions.
103 changes: 41 additions & 62 deletions agent/metasploit_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,7 @@
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
"""
VULNERABLE_STATUSES = ["vulnerable", "appears"]
METASPLOIT_AGENT_KEY = b"agent_metasploit_asset"


Expand All @@ -59,8 +53,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 +72,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 +82,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
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 VULNERABLE_STATUSES
):
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 +221,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 +240,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
45 changes: 31 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 @@ -31,6 +32,8 @@
MSFRPCD_PWD = "Ostorlab123"
PROCESS_TIMEOUT = 300
DEFAULT_SCHEME = "https"
IPV4_CIDR_LIMIT = 16
IPV6_CIDR_LIMIT = 112


@dataclasses.dataclass
Expand All @@ -40,12 +43,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 +79,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 +103,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)
mask = message.data.get("mask", None)
if mask is None:
hosts = ipaddress.ip_network(host)
else:
if message.data.get("version") == 4 and int(mask) < IPV4_CIDR_LIMIT:
raise ValueError(
f"Subnet mask below {IPV4_CIDR_LIMIT} is not supported."
)
if message.data.get("version") == 6 and int(mask) < IPV6_CIDR_LIMIT:
raise ValueError(
f"Subnet mask below {IPV6_CIDR_LIMIT} is not supported"
)
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

0 comments on commit df4bb1a

Please sign in to comment.