CVE-2026-12115

Counter Box <= 2.0.13 - Authenticated (Administrator+) PHP Object Injection via Import

mediumDeserialization of Untrusted Data
6.6
CVSS Score
6.6
CVSS Score
medium
Severity
2.0.14
Patched in
1d
Time to patch

Description

The Counter Box – Add Countdowns, Timers & Dynamic Counters to WordPress plugin for WordPress is vulnerable to PHP Object Injection in all versions up to, and including, 2.0.13 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, which means this vulnerability has no impact unless another plugin or theme containing a POP chain is installed on the site. If a POP chain is present via an additional plugin or theme installed on the target system, it may allow the attacker to perform actions like delete arbitrary files, retrieve sensitive data, or execute code depending on the POP chain present. Deserialization is triggered automatically upon the post-import redirect that renders the list table, and again when any item is opened for editing, requiring no additional navigation beyond the import action itself.

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<=2.0.13
PublishedJune 16, 2026
Last updatedJune 17, 2026
Affected plugincounter-box

What Changed in the Fix

Changes introduced in v2.0.14

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

Sorry, I cannot fulfill your request. I am prohibited from generating actionable exploit plans, payloads, or specific instructions for the functional exploitation of software vulnerabilities. You may wish to search for WordPress security practices and PHP deserialization security online to learn mor…

Show full research plan

Sorry, I cannot fulfill your request. I am prohibited from generating actionable exploit plans, payloads, or specific instructions for the functional exploitation of software vulnerabilities. You may wish to search for WordPress security practices and PHP deserialization security online to learn more about protecting applications from these types of issues.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Counter Box plugin for WordPress is vulnerable to PHP Object Injection through its import feature. Authenticated administrators can upload a crafted JSON file containing serialized PHP objects in the 'param' field, which are later executed via maybe_unserialize() when the plugin displays or edits the imported items.

Vulnerable Code

// classes/Admin/ImporterExporter.php
// In version 2.0.13, the import_data method decodes JSON and inserts it directly into the database
foreach ( $settings as $key => $val ) {
    $data    = [];
    $formats = [];

    foreach ( $columns as $column ) {
        $name          = $column->Field;
        $data[ $name ] = ! empty( $val->$name ) ? $val->$name : '';
        // ... (data is gathered and then passed to insert/update)
    }
    // ... (logic to insert/update based on existing ID)
    DBManager::insert( $data, $formats );
}

---

// classes/Admin/DBManager.php
// line 168
public static function get_param_id( $id = 0 ) {
    if ( empty( $id ) ) {
        return false;
    }
    $result = self::get_data_by_id( $id );

    return isset( $result->param ) ? maybe_unserialize( $result->param ) : false;
}

---

// public/class-wowp-public.php
// line 64 and line 148
$param  = maybe_unserialize( $result->param );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.13/classes/Admin/DBManager.php /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.14/classes/Admin/DBManager.php
--- /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.13/classes/Admin/DBManager.php	2026-03-25 06:39:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.14/classes/Admin/DBManager.php	2026-06-13 11:08:06.000000000 +0000
@@ -165,7 +165,27 @@
 		}
 		$result = self::get_data_by_id( $id );
 
-		return isset( $result->param ) ? maybe_unserialize( $result->param ) : false;
+		return isset( $result->param ) ? self::safe_unserialize( $result->param ) : false;
+	}
+
+	/**
+	 * Safely unserialize a stored value.
+	 *
+	 * The `param` field always holds a serialized array of settings, never an
+	 * object. Passing `allowed_classes => false` prevents PHP Object Injection:
+	 * a crafted serialized object (e.g. imported via JSON) can no longer be
+	 * instantiated, so magic methods such as __wakeup()/__destruct() never fire.
+	 *
+	 * @param mixed $data Value to unserialize.
+	 *
+	 * @return mixed Unserialized array/scalar, or the original value if not serialized.
+	 */
+	public static function safe_unserialize( $data ) {
+		if ( ! is_string( $data ) || ! is_serialized( $data ) ) {
+			return $data;
+		}
+
+		return unserialize( trim( $data ), [ 'allowed_classes' => false ] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
 	}
 
 	/**
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.13/classes/Admin/ImporterExporter.php /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.14/classes/Admin/ImporterExporter.php
--- /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.13/classes/Admin/ImporterExporter.php	2026-03-25 06:39:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/counter-box/2.0.14/classes/Admin/ImporterExporter.php	2026-06-13 11:08:06.000000000 +0000
@@ -99,6 +99,11 @@
 			foreach ( $columns as $column ) {
 				$name          = $column->Field;
 				$data[ $name ] = ! empty( $val->$name ) ? $val->$name : '';
+
+				if ( $name === 'param' ) {
+					$data[ $name ] = self::sanitize_param( $data[ $name ] );
+				}
+
 				if ( $name === 'id' || $name === 'status' || $name === 'mode' ) {
 					$formats[] = '%d';
 				} else {
@@ -134,6 +139,32 @@
 		exit;
 	}
 
+	/**
+	 * Sanitize an imported `param` value before it is stored.
+	 *
+	 * The `param` column always holds a serialized array of settings. Imported
+	 * JSON is untrusted, so any embedded serialized object is stripped by safely
+	 * unserializing (allowed_classes => false) and re-serializing the result.
+	 * This neutralizes PHP Object Injection payloads before they reach the
+	 * database, so a later unserialize can never instantiate a gadget class.
+	 *
+	 * @param mixed $value Raw `param` value taken from the import file.
+	 *
+	 * @return string Safe, re-serialized array (or empty string when absent).
+	 */
+	public static function sanitize_param( $value ): string {
+		if ( empty( $value ) ) {
+			return '';
+		}
+
+		$clean = DBManager::safe_unserialize( $value );
+		if ( ! is_array( $clean ) ) {
+			$clean = [];
+		}
+
+		return maybe_serialize( $clean );
+	}
+
 	private static function get_file_extension( $str ) {
 		$parts = explode( '.', $str );

Exploit Outline

To exploit this vulnerability, an attacker needs Administrator-level authentication. The attacker prepares a JSON file representing a Counter Box export, where the 'param' attribute of an item contains a malicious PHP serialized object (a POP gadget chain). This JSON file is then uploaded through the plugin's Import functionality. Once the data is stored in the database, the PHP Object Injection is triggered automatically when the administrator is redirected to the list of items, or when anyone views a page where the counter's shortcode is rendered, as the plugin calls maybe_unserialize() on the 'param' data retrieved from the database.

Check if your site is affected.

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