Post Grid Gutenberg Blocks for News, Magazines, Blog Websites – PostX <= 5.0.3 - Missing Authorization to Unauthenticated Sensitive Information Exposure
Description
The Post Grid Gutenberg Blocks for News, Magazines, Blog Websites – PostX plugin for WordPress is vulnerable to unauthorized access of data due to a missing capability check on the '/ultp/v2/get_dynamic_content/' REST API endpoint in all versions up to, and including, 5.0.3. This makes it possible for unauthenticated attackers to retrieve sensitive user metadata, including password hashes.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=5.0.3Source Code
WordPress.org SVNThis research plan outlines the steps to investigate and exploit **CVE-2025-12980**, a missing authorization vulnerability in the **PostX (ultimate-post)** plugin. This vulnerability allows unauthenticated attackers to access sensitive user data, including password hashes, via a specific REST API en…
Show full research plan
This research plan outlines the steps to investigate and exploit CVE-2025-12980, a missing authorization vulnerability in the PostX (ultimate-post) plugin. This vulnerability allows unauthenticated attackers to access sensitive user data, including password hashes, via a specific REST API endpoint.
1. Vulnerability Summary
- Vulnerability: Missing Authorization to Unauthenticated Sensitive Information Exposure.
- Affected Plugin: Post Grid Gutenberg Blocks for News, Magazines, Blog Websites – PostX (slug:
ultimate-post). - Vulnerable Versions: <= 5.0.3.
- Vulnerable Endpoint:
/wp-json/ultp/v2/get_dynamic_content/. - Root Cause: The REST API route
ultp/v2/get_dynamic_content/lacks a properpermission_callback(likely set to__return_trueor missing entirely), allowing unauthenticated users to trigger functions that retrieve user data without filtering sensitive fields likeuser_pass.
2. Attack Vector Analysis
- Endpoint:
/wp-json/ultp/v2/get_dynamic_content/ - HTTP Method:
POST(typical for Gutenberg dynamic fetching). - Parameters (Inferred):
content_type: Likely specifies what to fetch (e.g.,'user','author', or'post').idorobject_id: The ID of the user/post to fetch.attributesorsettings: Configuration for which fields to return.
- Authentication: None required (Unauthenticated).
- Preconditions: The REST API must be enabled (default in WordPress).
3. Code Flow (Inferred)
- Route Registration: The plugin registers the route using
register_rest_routein therest_api_inithook.- File: Likely
includes/classes/class-ultp-rest-api.phpor similar. - Code:
register_rest_route( 'ultp/v2', '/get_dynamic_content/', ... ).
- File: Likely
- Missing Check: The
permission_callbackfor this route is either absent or returnstruefor all users. - Data Retrieval: The callback function (likely
get_dynamic_content) receives thePOSTparameters. It uses these to call functions likeget_userdata()orget_user_meta(). - Information Leak: The function returns the resulting object directly in the REST response without sanitizing the
user_passfield or restricteduser_metakeys.
4. Nonce Acquisition Strategy
While REST API routes with permission_callback => '__return_true' often don't strictly require the wp_rest nonce for GET requests, POST requests in WordPress usually require a nonce if the user is authenticated, or the plugin might check a custom nonce for consistency.
- Identify Shortcode: PostX uses Gutenberg blocks. Create a page with a PostX block (e.g., a "Post Grid" or "Author" block).
- Create Test Page:
wp post create --post_type=page --post_title="PostX Test" --post_status=publish --post_content='<!-- wp:ultimate-post/post-grid /-->' - Navigate and Extract:
Use thebrowser_navigatetool to the new page. Usebrowser_evalto find localized data.- Search for:
ultp_ajax_obj,postx_obj, orultp_settings. - JavaScript:
window.ultp_ajax_obj?.nonceorwindow.postx_localize?.rest_nonce.
- Search for:
- Manual Check: If no nonce is found, the endpoint might be entirely unprotected.
5. Exploitation Strategy
Step 1: Discover Parameters
Since the exact parameter structure is inferred, first attempt to trigger an error or a successful response with a minimal payload to the endpoint.
Step 2: Craft Payload
Attempt to retrieve user ID 1 (typically the admin).
- Request URL:
http://localhost:8080/wp-json/ultp/v2/get_dynamic_content/ - Headers:
Content-Type: application/json - Payload (Inferred Candidate A):
{ "content_type": "user", "object_id": 1 } - Payload (Inferred Candidate B - based on PostX "Dynamic Data" feature):
{ "source": "user", "id": 1, "field": "all" }
Step 3: Execute HTTP Request
Use the http_request tool to send the POST request.
6. Test Data Setup
- Ensure PostX is active:
wp plugin activate ultimate-post. - Create a Target User: Ensure there is a user with a known ID.
wp user create victim victim@example.com --user_pass=Password123! --role=administrator - Identify User ID:
wp user list --field=ID --user_login=victim
7. Expected Results
- Success: A
200 OKresponse with a JSON object. - Sensitive Data: The JSON body should contain a key like
user_passordatacontaining a string starting with$P$or$wpb$(the WordPress password hash). It may also exposeuser_emailanduser_activation_key.
8. Verification Steps
After receiving the REST response:
- Compare Hash: Use WP-CLI to get the actual hash and verify it matches the leaked one.
wp db query "SELECT user_pass FROM wp_users WHERE ID = 1" - Check Meta: If metadata was leaked, verify it against:
wp user meta list 1
9. Alternative Approaches
If the get_dynamic_content endpoint requires specific "Block Settings" to be passed:
- Examine Source: Look for
render_dynamic_contentorget_dynamic_contentin the plugin folder (/wp-content/plugins/ultimate-post/) to see how it parses$request->get_params(). - Interception: If a page with a PostX block is loaded in the browser, check the "Network" tab in Playwright/Chrome to see if any requests are automatically made to
/ultp/v2/get_dynamic_content/and copy that request's structure. - Varying Content Types: Try
content_typevalues likeauthor,current_user, oruser_meta.
Summary
The PostX plugin for WordPress fails to implement authorization checks on its /ultp/v2/get_dynamic_content/ REST API endpoint. Unauthenticated attackers can exploit this to retrieve sensitive user data, including WordPress password hashes, by querying for specific user IDs through the dynamic content fetcher.
Vulnerable Code
// File: includes/classes/class-ultp-rest-api.php (inferred route registration) register_rest_route( 'ultp/v2', '/get_dynamic_content/', array( 'methods' => 'POST', 'callback' => array( $this, 'get_dynamic_content' ), 'permission_callback' => '__return_true', ) ); --- // File: includes/classes/class-ultp-rest-api.php (inferred callback logic) public function get_dynamic_content( $request ) { $params = $request->get_params(); $id = isset($params['id']) ? $params['id'] : 0; $type = isset($params['content_type']) ? $params['content_type'] : ''; if ($type === 'user') { return get_userdata($id); // Returns the full WP_User object including user_pass } // ... other logic }
Security Fix
@@ -25,7 +25,9 @@ register_rest_route( 'ultp/v2', '/get_dynamic_content/', array( 'methods' => 'POST', 'callback' => array( $this, 'get_dynamic_content' ), - 'permission_callback' => '__return_true', + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, ) );
Exploit Outline
To exploit this vulnerability, an unauthenticated attacker targets the WordPress REST API endpoint at /wp-json/ultp/v2/get_dynamic_content/. The attacker sends a POST request with a JSON payload containing parameters such as 'content_type' (set to 'user') and 'id' or 'object_id' (set to the target user's ID, e.g., 1 for the site administrator). Because the endpoint lacks a permission check and fails to sanitize the output, the response body will contain the full WordPress user object, which includes sensitive information like the 'user_pass' hash, 'user_email', and account metadata.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.