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

Agent Metasploit #4

Merged
merged 57 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
cf5c359
Agent core.
Oct 27, 2023
dcc3115
Add pymetasploit3
Oct 27, 2023
78e6de2
- Fix check bug
Oct 27, 2023
0acffe2
Fix unit tests
Oct 27, 2023
4579ef2
- Black!
Oct 27, 2023
a6f7ae9
Update docstring
Oct 27, 2023
bdc682e
Optimize imports
Oct 27, 2023
3af5017
Pass msf options as argument
Oct 27, 2023
60af5c7
Fix mypy
Oct 30, 2023
2744ea0
Black!
Oct 30, 2023
e7713c3
Merge remote-tracking branch 'origin/main' into agent_core
Nov 1, 2023
71a8147
Merge remote-tracking branch 'origin/feature/pymetasploit3' into agen…
Nov 1, 2023
3f3fd2a
use msfrpc init instead of fixture
Nov 1, 2023
a3e5da2
use port 55552
Nov 1, 2023
41b22b6
Kill previous instances of msfrpcd
Nov 1, 2023
faecf71
Fix linting
Nov 1, 2023
38d608f
options type array
Nov 1, 2023
2a8d5b3
Use metasploit module name for KB
Nov 1, 2023
9803d50
docstring details
Nov 1, 2023
ac27319
- Use python3.11
Nov 1, 2023
c88681a
- Test for IPv6
Nov 1, 2023
c808103
- Refactor utils
Nov 1, 2023
a01fd99
Use Kali mirrors instead of Rapid7
Nov 2, 2023
d626727
msfinstall custom script
Nov 2, 2023
3b60060
msfinstall custom script
Nov 2, 2023
65f4663
msfinstall custom script
Nov 2, 2023
a5354cf
msfinstall custom script
Nov 2, 2023
55c697f
msfdb
Nov 2, 2023
3424626
run as user
Nov 2, 2023
a3f5ff7
- Revert to msf script
Nov 2, 2023
f6c0c71
Merge remote-tracking branch 'origin/agent_core' into agent_core
Nov 2, 2023
4ea7a3d
- Revert to msf script
Nov 2, 2023
776a294
Unit tests
Nov 2, 2023
5f3868c
Change port
Nov 2, 2023
6c279f4
Address comments!
Nov 3, 2023
6314f00
msfrpc tweaks
Nov 3, 2023
1374c1d
Typing
Nov 3, 2023
70faa35
Merge remote-tracking branch 'origin/agent_core' into msfrpc_patch
Nov 3, 2023
1909396
Merge pull request #7 from Ostorlab/msfrpc_patch
3asm Nov 3, 2023
0933231
Remove arg
Nov 3, 2023
a9df478
- Update agent
Nov 3, 2023
365b3a1
Fix linter
Nov 3, 2023
5f46418
Use snap instead of apt
Nov 3, 2023
fa2ecc4
Linter
Nov 3, 2023
0c8191f
Don't capture output
Nov 3, 2023
7e84eb4
Skip test
Nov 3, 2023
03305ac
rollback
Nov 3, 2023
e142622
use Popen instead of run
Nov 7, 2023
47cc4d8
Run msfrpcd from pipeline
Nov 7, 2023
2be6437
- Use python3.11
Nov 7, 2023
3e245c2
Use code block
Nov 7, 2023
f7e0c67
Use code block
Nov 7, 2023
c594ec9
use timeout decorator
Nov 7, 2023
fcae758
Support multiple modules
Nov 8, 2023
d8943c2
Update docs
Nov 9, 2023
0e7249c
Remove timeout decorator
Nov 9, 2023
f8c10e4
Update Ostorlab yaml
Nov 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ jobs:
chmod +x msfinstall
sudo ./msfinstall
msfdb init
msfrpcd -P Ostorlab123 -p 55552
- name: Install dependencies.
run: |
python -m pip install --upgrade pip
Expand All @@ -51,4 +50,4 @@ jobs:
- 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's raising errors because of pymetasploit3 local module

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How hard would it be to add typing there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it has much value right now given the 90+ pending exploits we have.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BlueSquare1 you can ignore pymetasploit3 without ignoring everything.

warn_return_any = True
exclude = .*_pb2.py

[mypy-pymetasploit3]
ignore_missing_imports = True
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
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
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
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">
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
<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)
194 changes: 194 additions & 0 deletions agent/metasploit_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""Ostorlab Agent implementation for metasploit"""
import json
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__)

SCHEME_TO_PORT = {"http": 80, "https": 443}
DEFAULT_PORT = 443
MODULE_TIMEOUT = 180


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


class ArgumentError(Error):
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
"""Error when a required argument is missing"""
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved


class ModuleError(Error):
"""Errors related to metasploit modules"""
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved


class CheckError(Error):
"""Errors related to metasploit check method"""
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved


class MetasploitAgent(
agent.Agent, vuln_mixin.AgentReportVulnMixin, persist_mixin.AgentPersistMixin
):
"""Metasploit 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client is the msfrpc instance
cid is the current console id for metasploit


def process(self, message: m.Message) -> None:
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
"""Trigger Agent metasploit and emit 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.")
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved

vhost, rport = self._prepare_target(message)

try:
module_type, module_name = module.split("/", 1)
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.")
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved

job_uuid = job["uuid"]
started_timestamp = time.time()
results = None
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the proper way to timeout. This is supppppppppper CPU intensive.


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:
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
return
technical_detail += f"Message: {module_output}"

entry = kb.KB.WEB_GENERIC
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
entry.title = selected_module.name or "Metasploit generic vulnerability entry"
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

msf_options = json.loads(self.args.get("options") or "[]")
for arg in msf_options:
arg_name = arg["name"]
if arg_name in selected_module.options:
selected_module[arg_name] = arg["value"]

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]:
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
BlueSquare1 marked this conversation as resolved.
Show resolved Hide resolved
"""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
Loading