CVE-2026-59521

Real Testimonials – Testimonial Slider, Collect Customer Reviews and Video Testimonials <= 3.1.15 - Authenticated (Administrator+) PHP Object Injection

mediumDeserialization of Untrusted Data
6.6
CVSS Score
6.6
CVSS Score
medium
Severity
3.1.16
Patched in
8d
Time to patch

Description

The Real Testimonials – Testimonial Slider, Collect Customer Reviews and Video Testimonials plugin for WordPress is vulnerable to PHP Object Injection in versions up to, and including, 3.1.15 via deserialization of untrusted input. This makes it possible for authenticated attackers, with administrator-level access and above, to inject a PHP Object. No known POP chain is present in the vulnerable software. If a POP chain is present via an additional plugin or theme installed on the target system, it could allow the attacker to delete arbitrary files, retrieve sensitive data, or execute code.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.1.15
PublishedJuly 7, 2026
Last updatedJuly 14, 2026
Affected plugintestimonial-free

What Changed in the Fix

Changes introduced in v3.1.16

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-59521 ## 1. Vulnerability Summary The **Real Testimonials** plugin (versions <= 3.1.15) is vulnerable to **PHP Object Injection** via the deserialization of untrusted input. The vulnerability typically resides in the "Tools" or "Settings" import functionality…

Show full research plan

Exploitation Research Plan - CVE-2026-59521

1. Vulnerability Summary

The Real Testimonials plugin (versions <= 3.1.15) is vulnerable to PHP Object Injection via the deserialization of untrusted input. The vulnerability typically resides in the "Tools" or "Settings" import functionality, where the plugin uses the SPFTESTIMONIAL (Codestar-like) framework to handle configuration imports. By providing a crafted serialized string—often Base64 encoded—to the import field, an authenticated administrator can trigger the unserialize() sink. While no POP chain is identified in the plugin itself, any chain present in the WordPress environment (other plugins/themes) can be leveraged for RCE or file manipulation.

2. Attack Vector Analysis

  • Target Endpoint: /wp-admin/edit.php?post_type=spt_testimonial&page=testimonial_tools
  • Vulnerable Parameter: The import data field, typically named after the framework prefix: sp_testimonial_pro_tools[import] or a similar key within the POST body.
  • Authentication: Required (Administrator+).
  • Preconditions: The attacker must have access to the "Tools" submenu under the "Real Testimonials" menu.

3. Code Flow

  1. Registration: The "Tools" page is registered in src/Admin/Views/Framework/configs/tools.php with the unique prefix sp_testimonial_pro_tools.
  2. Framework Initialization: The SPFTESTIMONIAL_Options class (in src/Admin/Views/Framework/Classes/options.class.php) initializes the settings page for this prefix.
  3. Import Logic: When the "Import" form is submitted, the framework processes the input. In the underlying framework (Codestar derivative), the import field type triggers a block of code that:
    • Retrieves the string from the POST request or uploaded file.
    • Decodes it (if Base64 encoded).
    • Passes it to unserialize() or maybe_unserialize() to restore the option array.
  4. Sink: The unserialize() function is called on the attacker-controlled string, leading to object instantiation.

4. Nonce Acquisition Strategy

The "Tools" page uses a nonce for form security.

  1. Identify Page: The "Tools" page is located at edit.php?post_type=spt_testimonial&page=testimonial_tools.
  2. Locate Nonce: The framework typically embeds a nonce in a hidden field named _wpnonce or similar, tied to the prefix sp_testimonial_pro_tools.
  3. Extraction:
    • Navigate to the Tools page.
    • Use browser_eval to extract the nonce from the form.
    • Variable check: Look for window.sp_testimonial_pro_tools_vars or simply the hidden input field.
// Example extraction via browser_eval
document.querySelector('input[name="_wpnonce"]')?.value || 
document.querySelector('input[name="sp_testimonial_pro_tools_nonce"]')?.value;

5. Exploitation Strategy

Step 1: Preparation

Construct a simple PHP Object Injection payload. Since no specific POP chain is required for the PoC, we use stdClass.

  • Payload: O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}
  • Encoding: Base64 encode the payload if the framework expects it (likely): Tzo4OiJzdGRDbGFzcyI6MTp7czozOiJmb28iO3M6MzoiYmFyIjt9

Step 2: HTTP Request

Use the http_request tool to send a POST request to the Tools page.

  • URL: https://<target>/wp-admin/edit.php?post_type=spt_testimonial&page=testimonial_tools
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    sp_testimonial_pro_tools%5Bimport%5D=Tzo4OiJzdGRDbGFzcyI6MTp7czozOiJmb28iO3M6MzoiYmFyIjt9&_wpnonce=<NONCE>&sp_testimonial_pro_tools_import_button=1
    
    (Note: Parameter names like sp_testimonial_pro_tools_import_button are inferred from standard framework behavior and should be verified via browser_eval on the page source.)

6. Test Data Setup

  1. Plugin Installation: Ensure "Real Testimonials" <= 3.1.15 is installed and activated.
  2. User: An Administrator user is required for the session.
  3. Page Creation: No specific frontend content is needed as the vulnerability is in the Admin backend.

7. Expected Results

  • The server processes the request.
  • If a debugger like Xdebug is active or the object has a __wakeup method that triggers an error, the error will be visible.
  • In a successful "blind" injection, the framework might attempt to replace existing options with the "array" it expected, potentially breaking plugin settings (a sign that the data was processed).

8. Verification Steps

Since this is a deserialization vulnerability without a confirmed POP chain in the plugin, verification involves confirming the sink is reached:

  1. Error Logs: Check wp-content/debug.log for "Notice: unserialize(): Error at offset..." if the payload is slightly malformed, confirming unserialize was called.
  2. Database Check: If the injection was intended to overwrite options, use wp option get sp_testimonial_pro_tools to see if the data changed.
  3. Dummy Class: Create a temporary file in the plugin directory containing a dummy class with a __destruct method that writes to a file, then try to inject that class.

9. Alternative Approaches

  • File Upload: The tools.php file defines the import as "Import JSON/CSV File". If the POST parameter fails, try uploading a .json file containing the serialized string via a multipart/form-data request to the same endpoint.
  • Metabox Injection: Check if the save_meta_box function in src/Admin/Views/Framework/Classes/metabox.class.php (Line 115) accepts serialized strings for fields with 'data_type' => 'serialize'. If so, updating a Testimonial post via the REST API or Admin editor with a crafted meta value could trigger the vulnerability upon the next page load.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Real Testimonials plugin for WordPress is vulnerable to PHP Object Injection in versions up to and including 3.1.15 due to the use of the unsafe `maybe_unserialize` function on user-supplied import data. Authenticated administrators can exploit this by submitting crafted serialized objects through the plugin's 'Tools' import feature, potentially leading to remote code execution or file manipulation if a suitable POP chain exists on the site.

Vulnerable Code

// src/Includes/Import_Export.php:133
$export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] =
isset( $export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] )
	? maybe_unserialize( $export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] )
	: '';

---

// src/Includes/Import_Export.php:335
if ( is_string( $value ) ) {
	$value = maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/testimonial-free/3.1.15/src/Includes/Import_Export.php /home/deploy/wp-safety.org/data/plugin-versions/testimonial-free/3.1.16/src/Includes/Import_Export.php
--- /home/deploy/wp-safety.org/data/plugin-versions/testimonial-free/3.1.15/src/Includes/Import_Export.php	2025-09-01 08:09:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/testimonial-free/3.1.16/src/Includes/Import_Export.php	2026-07-07 07:05:28.000000000 +0000
@@ -130,7 +130,7 @@
 			foreach ( $export['shortcode'] as $key => $value ) {
 				$export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] =
 				isset( $export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] )
-					? maybe_unserialize( $export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] )
+					? $this->maybe_unserialize( $export['shortcode'][ $key ]['meta']['sp_tpro_meta_options'] )
 					: '';
 			}
 		}
@@ -153,32 +153,20 @@
 		wp_send_json( $export, 200 );
 	}
 
-
 	/**
-	 * Get page by title
+	 * Maybe unserialize data if it is serialized.
 	 *
-	 * @param string $page_title Page title.
-	 * @param string $output Optional.
-	 * @param string $post_type Post type.
-	 * @return obj.
+	 * @param  mixed $data Data to maybe unserialize.
+	 * @param  mixed $options Options for unserialization.
+	 * @return mixed
 	 */
-	public function sp_testimonial_get_page_by_title( $page_title, $output = OBJECT, $post_type = 'page' ) {
-		global $wpdb;
-		$sql  = $wpdb->prepare(
-			"
-			SELECT ID
-			FROM $wpdb->posts
-			WHERE post_title = %s
-			AND post_type = %s
-		",
-			$page_title,
-			$post_type
-		);
-		$page = $wpdb->get_var( $sql ); // phpcs:ignore -- WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
-		if ( $page ) {
-			return get_post( $page, $output );
+	public function maybe_unserialize( $data, $options = array() ) {
+		if ( is_serialized( $data ) ) {
+			$default_option = array( 'allowed_classes' => false );
+			$options        = wp_parse_args( $options, $default_option );
+			return unserialize( $data, $options ); // phpcs:ignore -- dissallowed any classes.
 		}
-		return null;
+		return $data;
 	}
 
 	/**
@@ -332,7 +316,7 @@
 				if ( isset( $shortcode['meta'] ) && is_array( $shortcode['meta'] ) ) {
 					foreach ( $shortcode['meta'] as $key => $value ) {
 						if ( is_string( $value ) ) {
-							$value = maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );
+							$value = $this->maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );
 						}
 						update_post_meta(
 							$new_shortcode_id,

Exploit Outline

1. Authenticate as an Administrator and navigate to the 'Tools' page under the 'Real Testimonials' menu (`/wp-admin/edit.php?post_type=spt_testimonial&page=testimonial_tools`). 2. Prepare a PHP Object Injection payload, such as a serialized object using `stdClass` or any valid POP chain present in the environment (e.g., `O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}`). 3. Base64 encode the serialized string if required by the framework's import handler. 4. Extract the security nonce (`_wpnonce`) from the page's HTML form. 5. Send a POST request to the Tools page including the crafted string in the `sp_testimonial_pro_tools[import]` parameter (or the relevant framework import field) and the valid nonce. This will trigger the `maybe_unserialize` sink in the `Import_Export.php` class during processing.

Check if your site is affected.

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