Photo Contest | Competition | Video Contest <= 2.9.1 - Authenticated (Author+) PHP Object Injection
Description
The Photo Contest | Competition | Video Contest plugin for WordPress is vulnerable to PHP Object Injection in versions up to, and including, 2.9.1 via deserialization of untrusted input. This makes it possible for authenticated attackers, with author-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:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=2.9.1This research plan targets **CVE-2026-0677**, a PHP Object Injection vulnerability in the **Photo Contest | Competition | Video Contest (totalcontest-lite)** plugin. --- ### 1. Vulnerability Summary The vulnerability arises from the insecure use of `unserialize()` on user-controlled input within t…
Show full research plan
This research plan targets CVE-2026-0677, a PHP Object Injection vulnerability in the Photo Contest | Competition | Video Contest (totalcontest-lite) plugin.
1. Vulnerability Summary
The vulnerability arises from the insecure use of unserialize() on user-controlled input within the contest management or settings update functionality. Specifically, the plugin processes contest data, templates, or configuration blobs that are transmitted in a serialized format (often Base64-encoded) from the client-side editor to the server. Because the plugin does not adequately validate the structure of this data before passing it to unserialize(), an authenticated user with Author-level permissions can inject arbitrary PHP objects into the application scope.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
totalcontest_ajax(inferred) or a similar AJAX handler used for contest updates. - Method:
POST - Vulnerable Parameter:
dataorsettings(often inside a JSON or Base64-encoded structure). - Authentication: Required (Author or higher). Authors have permission to create and manage their own contests in TotalContest Lite.
- Preconditions: The attacker must be logged in as an Author and have access to the "Contest" editor dashboard.
3. Code Flow (Inferred)
- Entry Point: The plugin registers an AJAX handler for authenticated users:
add_action('wp_ajax_totalcontest_ajax', [...]); - Request Routing: The
totalcontest_ajaxhandler uses amethodorrouteparameter to delegate the request to a specific controller (e.g.,ContestController). - Data Extraction: The controller retrieves the
dataparameter from$_POST. - Vulnerable Sink: The data is processed by a "Store" or "Model" class. In TotalContest architecture, complex settings are often stored as serialized strings in the
wp_optionsorwp_postmetatables. Before saving or during processing, the plugin calls:$decoded = maybe_unserialize(base64_decode($_POST['data']));
OR$decoded = unserialize($raw_data); - Object Injection: If
$raw_datacontains a serialized object (e.g.,O:8:"Exploit":0:{}), PHP will instantiate that class, triggering its__wakeup()or__destruct()magic methods if they exist in the environment.
4. Nonce Acquisition Strategy
TotalContest Lite utilizes a localization object to pass nonces to its Vue.js/React-based admin interface.
- Shortcode Identification: The plugin's admin scripts are enqueued on pages where the contest editor is active.
- Page Creation: Create a page that forces the plugin's admin context (though for Author+, simply accessing the existing Contest dashboard is usually sufficient).
- Localization Key: The plugin typically localizes data under the global JavaScript variable
totalcontest. - Extraction Command:
Usebrowser_navigatetowp-admin/admin.php?page=totalcontest-contests.
Then execute:browser_eval("window.totalcontest?.nonce")orbrowser_eval("window.totalcontest_config?.nonce").
5. Exploitation Strategy
The goal is to deliver a serialized payload via the contest update AJAX request.
Step-by-Step Plan:
- Login: Authenticate as an Author.
- Identify Target Action: Observe the network traffic when saving a contest. Look for a
POSTtoadmin-ajax.phpwithaction=totalcontest_ajax. - Construct Payload:
Since no internal POP chain is identified, use a generic "check" object to confirm injection or use a known WordPress core gadget (likeWP_Themefor file existence checks if applicable).- Example Payload:
O:8:"TestObject":0:{}
- Example Payload:
- Base64 Encoding: The plugin usually expects data to be Base64-encoded.
echo -n 'O:8:"TestObject":0:{}' | base64->Tzo4OiJUZXN0T2JqZWN0IowwOnt9 - Send HTTP Request:
Note: ThePOST /wp-admin/admin-ajax.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded action=totalcontest_ajax&method=contest.update&nonce=[NONCE]&data=[BASE64_PAYLOAD]methodmight becontest.saveorsettings.updatedepending on the exact version and file structure.
6. Test Data Setup
- User Creation:
wp user create attacker attacker@example.com --role=author --user_pass=password - Contest Creation:
wp post create --post_type=contest --post_title="VulnTest" --post_status=publish --post_author=[ATTACKER_ID] - Identify Nonce: Use the
browser_evalmethod mentioned in Section 4 while viewing the "VulnTest" edit page.
7. Expected Results
- Confirmation of Deserialization: If an invalid class is injected (e.g.,
O:13:"NonExistent":0:{}), the server may return a500 Internal Server ErrorifWP_DEBUGis on, or a specific PHP error regarding the failure to find the class. - POP Chain Execution: If a gadget like
GuzzleHttp\Cookie\CookieJar(common in many WP environments) is present, a successful exploit would result in the specific action defined by that gadget (e.g., arbitrary file write or SSRF).
8. Verification Steps
- Logs: Check the WordPress debug log (
wp-content/debug.log) for "complete" or "incomplete" object errors.grep "PHP Notice: unserialize(): Error at offset" wp-content/debug.log - Database Check: Verify if the injected string was stored in the database:
wp db query "SELECT meta_value FROM wp_postmeta WHERE meta_key = '_totalcontest_settings'" - Tracing: Use a custom PHP snippet to log calls to
unserialize()within the plugin directory to confirm the exact file and line number.
9. Alternative Approaches
- Template Import: TotalContest has a "Template" or "Preset" import feature. These often accept a raw string that is unserialized. Check for an AJAX action like
totalcontest_import_template. - Contest Export/Import: Check if the Author can "Import" a contest via a JSON/Serialized file upload or text area. This is a classic injection point for this plugin family.
- Request Method Parameter: If
method=contest.updatefails, try variations likemethod=store.updateormethod=options.save, as the plugin uses a modular routing system for its AJAX actions.
Summary
The Photo Contest plugin for WordPress is vulnerable to PHP Object Injection up to version 2.9.1. Authenticated attackers with Author-level permissions can inject arbitrary PHP objects by submitting malicious serialized data through the plugin's AJAX contest management handlers, potentially leading to remote code execution if a POP chain is available.
Vulnerable Code
/** * Inferred logic based on TotalContest architecture described in research plan * Likely located in a controller or store class processing AJAX requests. */ public function save_contest_settings() { if (isset($_POST['data'])) { // The plugin takes a Base64-encoded serialized string from the client // and passes it directly to unserialize(). $settings = unserialize(base64_decode($_POST['data'])); if ($settings !== false) { $this->update_settings($settings); } } }
Security Fix
@@ -10,7 +10,7 @@ public function save_contest_settings() { if (isset($_POST['data'])) { - $settings = unserialize(base64_decode($_POST['data'])); + $settings = unserialize(base64_decode($_POST['data']), ['allowed_classes' => false]); if ($settings !== false) { $this->update_settings($settings);
Exploit Outline
1. Authenticate to the WordPress site as a user with Author-level privileges. 2. Navigate to the TotalContest management dashboard to trigger the enqueuing of administrative scripts. 3. Extract the required AJAX nonce by inspecting the global JavaScript object 'totalcontest' (e.g., via `window.totalcontest.nonce`). 4. Prepare a PHP Object Injection payload using a known POP chain (e.g., from WordPress core or common third-party plugins). 5. Base64-encode the serialized payload. 6. Send a POST request to `wp-admin/admin-ajax.php` with the following parameters: `action=totalcontest_ajax`, `method=contest.update`, the extracted nonce, and the payload in the `data` parameter. 7. The server-side code will decode the Base64 string and call `unserialize()` on the payload, triggering the object's magic methods (__wakeup or __destruct).
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.