Skip to content

Commit

Permalink
Agent core.
Browse files Browse the repository at this point in the history
  • Loading branch information
ostorlab committed Oct 27, 2023
1 parent 7930532 commit cf5c359
Show file tree
Hide file tree
Showing 12 changed files with 681 additions and 107 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Metasploit.
run: |
sudo apt update
sudo apt install curl gpg
curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > msfinstall
chmod +x msfinstall
sudo ./msfinstall
msfdb init
msfrpcd -P Ostorlab123 -p 55552
- name: Install dependencies.
run: |
python -m pip install --upgrade pip
python -m pip install -e tools/pymetasploit3
python -m pip install -r requirement.txt
python -m pip install -r tests/test-requirement.txt
- name: Runnning tests with pytest.
- name: Running tests with pytest.
run: |
set -o pipefail
pytest -m "not docker"
pytest -m "not docker" tests/
4 changes: 3 additions & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ warn_incomplete_stub = True
warn_redundant_casts = True
#warn_unreachable = True
warn_unused_ignores = True
disallow_any_unimported = True
disallow_any_unimported = False
warn_return_any = True
exclude = .*_pb2.py

[mypy-pymetasploit3]
ignore_missing_imports = True
17 changes: 8 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
FROM python:3.10-alpine as base
FROM base as builder
RUN apk add build-base
RUN mkdir /install
WORKDIR /install
FROM kalilinux/kali-rolling:latest
RUN apt-get update && apt-get install -y python3 \
python3-pip \
metasploit-framework
COPY requirement.txt /requirement.txt
RUN pip install --prefix=/install -r /requirement.txt
FROM base
COPY --from=builder /install /usr/local
RUN python3 -m pip install -r /requirement.txt
COPY tools /tools
RUN pip install -e /tools/pymetasploit3
RUN mkdir -p /app/agent
ENV PYTHONPATH=/app
COPY agent /app/agent
COPY ostorlab.yaml /app/agent/ostorlab.yaml
WORKDIR /app
CMD ["python3", "/app/agent/template_agent.py"]
CMD ["python3", "/app/agent/metasploit_agent.py"]
106 changes: 75 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,80 @@
# Ostorlab Template Agent
<h1 align="center">Agent Metasploit</h1>

This repo is a template to build an Ostorlab agent in Python. It ships with good best practices like:
<p align="center">
<img src="https://img.shields.io/badge/License-Apache_2.0-brightgreen.svg">
<img src="https://img.shields.io/github/languages/top/ostorlab/agent_metasploit">
<img src="https://img.shields.io/github/stars/ostorlab/agent_metasploit">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
</p>

* Github actions workflow
* Linting checks
* Static typing checks with Mypy
* Running unit test with Pytest
* Compute test coverage
_Metasploit is a powerful penetration testing framework._

---

<p align="center">
<img src="https://github.com/Ostorlab/agent_metasploit/blob/main/images/logo.png" alt="agent-metasploit" />
</p>

This repository is an implementation of [Ostorlab Agent](https://pypi.org/project/ostorlab/) for the [Metasploit Framework](https://github.com/rapid7/metasploit-framework) by Rapid7.

## Getting Started
To perform your first scan, simply run the following command:
```shell
ostorlab scan run --install --agent agent/ostorlab/metasploit ip 8.8.8.8
```

This command will download and install `agent/ostorlab/metasploit` and target the ip `8.8.8.8`.
For more information, please refer to the [Ostorlab Documentation](https://github.com/Ostorlab/ostorlab/blob/main/README.md)


## Usage

Agent Metasploit can be installed directly from the ostorlab agent store or built from this repository.

### Install directly from ostorlab agent store

```shell
ostorlab agent install agent/ostorlab/metasploit
```

You can then run the agent with the following command:
```shell
ostorlab scan run --agent agent/ostorlab/metasploit ip 8.8.8.8
```


### Build directly from the repository

1. To build the metasploit agent you need to have [ostorlab](https://pypi.org/project/ostorlab/) installed in your machine. if you have already installed ostorlab, you can skip this step.

```shell
pip3 install ostorlab
```

2. Clone this repository.

```shell
git clone https://github.com/Ostorlab/agent_metasploit.git && cd agent_metasploit
```

3. Build the agent image using ostorlab cli.

```shell
ostorlab agent build --file=ostorlab.yaml
```

You can pass the optional flag `--organization` to specify your organisation. The organization is empty by default.

4. Run the agent using on of the following commands:
* If you did not specify an organization when building the image:
```shell
ostorlab scan run --agent agent//metasploit ip 8.8.8.8
```
* If you specified an organization when building the image:
```shell
ostorlab scan run --agent agent/[ORGANIZATION]/metasploit ip 8.8.8.8
```


Here are links to good resources to get started:

* [Write An Agent](https://docs.ostorlab.co/tutorials/write-an-ostorlab-agent.html)
* [Use Ostorlab](https://docs.ostorlab.co/tutorials/run-your-first-scan.html)
* [Debugging and Testing Agents](https://docs.ostorlab.co/tutorials/debugging-agents.html)
* [Ostorlab Internals](https://docs.ostorlab.co/tutorials/life-of-a-scan.html)

## Ideas for Agents to build

Implementation of popular tools like:

* [semgrep](https://github.com/returntocorp/semgrep) for source code scanning.
* [nbtscan](http://www.unixwiz.net/tools/nbtscan.html): Scans for open NETBIOS nameservers on your target’s network.
* [onesixtyone](https://github.com/trailofbits/onesixtyone): Fast scanner to find publicly exposed SNMP services.
* [Retire.js](http://retirejs.github.io/retire.js/): Scanner detecting the use of JavaScript libraries with known
vulnerabilities.
* [snallygaster](https://github.com/hannob/snallygaster): Finds file leaks and other security problems on HTTP servers.
* [testssl.sh](https://testssl.sh/): Identify various TLS/SSL weaknesses, including Heartbleed, CRIME and ROBOT.
* [TruffleHog](https://github.com/trufflesecurity/truffleHog): Searches through git repositories for high entropy
strings and secrets, digging deep into commit history.
* [cve-bin-tool](https://github.com/intel/cve-bin-tool): Scan binaries for vulnerable components.
* [XSStrike](https://github.com/s0md3v/XSStrike): XSS web vulnerability scanner with generative payload.
* ~~[Subjack](https://github.com/haccer/subjack): Subdomain takeover scanning tool.~~
* [DnsReaper](https://github.com/punk-security/dnsReaper): Subdomain takeover scanning tool.
## License
[Apache](./LICENSE)
193 changes: 193 additions & 0 deletions agent/metasploit_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""Ostorlab Agent implementation for metasploit"""
import logging
import socket
import time
from urllib import parse as urlparser

from ostorlab.agent import agent, definitions as agent_definitions
from ostorlab.agent.kb import kb
from ostorlab.agent.message import message as m
from ostorlab.agent.mixins import agent_persist_mixin as persist_mixin
from ostorlab.agent.mixins import agent_report_vulnerability_mixin as vuln_mixin
from ostorlab.runtimes import definitions as runtime_definitions
from rich import logging as rich_logging

from agent import utils
from pymetasploit3 import msfrpc

logging.basicConfig(
format="%(message)s",
datefmt="[%X]",
level="INFO",
force=True,
handlers=[rich_logging.RichHandler(rich_tracebacks=True)],
)
logger = logging.getLogger(__name__)

AGENT_ARGS = ["module", "RHOSTS", "VHOST", "RPORT"]
SCHEME_TO_PORT = {"http": 80, "https": 443}
DEFAULT_PORT = 443
MODULE_TIMEOUT = 180


class Error(Exception):
"""Base custom error class."""


class ArgumentError(Error):
"""Error when a required argument is missing"""


class ModuleError(Error):
"""Errors related to metasploit modules"""


class CheckError(Error):
"""Errors related to metasploit check method"""


class MetasploitAgent(
agent.Agent, vuln_mixin.AgentReportVulnMixin, persist_mixin.AgentPersistMixin
):
"""Source map agent."""

def __init__(
self,
agent_definition: agent_definitions.AgentDefinition,
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)
self.client = utils.initialize_msf_rpc()
self.cid = self.client.consoles.console().cid

def process(self, message: m.Message) -> None:
"""Trigger Source map enumeration and emit found findings
Args:
message: A message containing the path and the content of the file to be processed
"""
module = self.args.get("module")
if module is None:
raise ArgumentError("Metasploit module must be specified.")

vhost, rport = self._prepare_target(message)

module_type, module_name = module.split("/", 1)
try:
selected_module = self.client.modules.use(module_type, module_name)
except msfrpc.MsfRpcError as exc:
raise ModuleError("Specified module does not exist") from exc

logger.info("Selected metasploit module: %s", selected_module.modulename)
selected_module = self._set_module_args(selected_module, vhost, rport)

if module_type == "exploit":
mode = "check"
job = selected_module.check_exploit()
elif module_type == "auxiliary":
mode = "exploit"
job = selected_module.execute()
else:
raise ArgumentError("Metasploit module should be exploit or auxiliary.")

job_uuid = job["uuid"]
started_timestamp = time.time()
results = None
while True:
job_result = self.client.jobs.info_by_uuid(job_uuid)
status = job_result["status"]
if status == "completed":
results = job_result["result"]
break
if status == "errored":
logger.error("Encountered an unexpected error: %s", job_result["error"])
break
if time.time() - started_timestamp > MODULE_TIMEOUT:
raise CheckError(f"Timeout while running job: {job_uuid}")
time.sleep(5)

if isinstance(results, dict) and results.get("code") == "safe":
return

technical_detail = f"Using `{module_type}` module `{module_name}`\n"
technical_detail += f"Target: {vhost}\n"

if isinstance(results, dict) and results.get("code") == "vulnerable":
technical_detail += f'Message: {results["message"]}'
else:
console_output = self.client.consoles.console(
self.cid
).run_module_with_output(selected_module, mode=mode)
module_output = console_output.split("WORKSPACE => Ostorlab")[1]
if "[-]" in module_output:
return
technical_detail += f"Message: {module_output}"

entry = kb.KB.WEB_GENERIC
entry.title = "Metasploit vulnerability detection"
self.report_vulnerability(
entry=entry,
technical_detail=technical_detail,
risk_rating=vuln_mixin.RiskRating.HIGH,
)

def _set_module_args(
self, selected_module: msfrpc.MsfModule, vhost: str, rport: int
) -> msfrpc.MsfModule:
rhost = socket.gethostbyname(vhost)
if "RHOSTS" not in selected_module.required:
raise ArgumentError(
f"Argument not implemented, accepted args: {str(selected_module.required)}"
)
selected_module["RHOSTS"] = rhost
if "VHOST" in selected_module.options:
selected_module["VHOST"] = vhost
if "RPORT" in selected_module.missing_required:
selected_module["RPORT"] = rport

extra_args = [arg_name for arg_name in self.args if arg_name not in AGENT_ARGS]
for arg in extra_args:
if arg in selected_module.required:
selected_module[arg] = self.args.get(arg)

if len(selected_module.missing_required) > 0:
raise ArgumentError(
f"The following arguments are missing: {str(selected_module.missing_required)}"
)

return selected_module

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

def _prepare_target(self, message: m.Message) -> tuple[str, int]:
"""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 = self._get_port(message)
return host, port
elif (host := message.data.get("name")) is not None:
port = self._get_port(message)
return host, port
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
else:
raise NotImplementedError


if __name__ == "__main__":
logger.info("Starting Agent ...")
MetasploitAgent.main()
Loading

0 comments on commit cf5c359

Please sign in to comment.