Counter Box <= 2.0.13 - Authenticated (Administrator+) PHP Object Injection via Import
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:HTechnical Details
What Changed in the Fix
Changes introduced in v2.0.14
Source Code
WordPress.org SVNSorry, 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.
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
@@ -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 } /** @@ -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.