## https://sploitus.com/exploit?id=D0D2BEBC-92CA-5CE2-87A6-E7A2ACAFC53B
```
โโ โโ
โโโโโโ โโ โโ โโ
โโ โโ โโโโโโโโ โโ โโ โ
โโโโโโ โโ โโ โโโโโ โโ โโ โโโโโ โโโโโ
โ โโ โโ โโ โโ โโโโโ โโ โโ โโโโโ โโ
โโโโ โโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โปโโโโโโโโโโโโโโ โโฉโ โโโโโโโโโโโโโโโบ
CVE-2025-53328
Developed by @XoanOuteiro
At https://github.com/attelier
โปโโโโโโโโโโโโโโ โโฉโ โโโโโโโโโโโโโโโบ
```
# CVE-2025-53328 โ Unauthenticated Local File Inclusion in Opinion Stage
## Overview
| Field | Value |
|--------------------|------------------------------------------------------------------------------------|
| CVE | CVE-2025-53328 |
| Plugin | social-polls-by-opinionstage (Opinion Stage Poll, Survey & Quiz Maker) |
| Vulnerable version | โค 19.11.0 |
| Patched in | 19.11.1 |
| Type | Local File Inclusion (Unauthenticated) โ CWE-98 |
| CVSS | 7.5โ8.1 (High) |
| Original reporter | Ananda Dhakal (Patchstack) |
| CVE published | 26 Aug 2025 |
The Opinion Stage plugin for WordPress (โค 19.11.0) contains an unauthenticated Local File Inclusion vulnerability in `admin/admin-page-loader.php`. A `$_REQUEST` parameter is passed through `sanitize_text_field()` โ which does not filter path traversal sequences โ and used to construct a path for `include_once`. The vulnerable code is reachable without authentication via `admin-ajax.php` or `admin-post.php`.
---
## Vulnerable file identification
Diffing version 19.8.20 against 19.11.1:
```bash
diff -rq social-polls-by-opinionstage/ patched/social-polls-by-opinionstage/
```
The patch removes `admin/admin-page-loader.php` entirely, along with `admin/resources/` and `admin/helpers/`. The admin page-loading architecture was replaced by `src/Modules/Admin.php`.
---
## Source analysis
### Sink โ `OpinionStageAdminPageLoader` class in `admin/admin-page-loader.php`
**Parameter ingestion** (`prepare_slug()`):
```
Line 71: if ( ! empty( $_REQUEST['page'] ) ) {
Line 72: $qry_str_check_os = sanitize_text_field( $_REQUEST['page'] );
Line 75: $this->slug = str_replace( 'opinionstage-', '', sanitize_text_field( $_REQUEST['page'] ) );
```
The method reads `$_REQUEST['page']`, verifies that it contains the `opinionstage-` prefix, strips the prefix, and stores the remainder as `$this->slug`. The only sanitization applied is `sanitize_text_field()`, which strips HTML tags and normalizes whitespace but does not filter directory traversal characters.
**File inclusion** (`load_file()`):
```php
include_once plugin_dir_path(__FILE__) . 'resources/' . $this->slug . '.php';
```
**Secondary inclusion** (`maybe_load_template_file()`):
Includes from `helpers/` and `views/` using the same unsanitized slug.
**Tertiary vector** (`load_assets()`):
```php
call_user_func('opinionstage_' . $slug . '_load_resources');
```
Dynamic function call constructed from the unsanitized slug.
### Loading chain
```
plugin.php
โโ if (is_admin()) โ require 'admin/init.php'
โโ admin/init.php (line 13) โ require 'admin-page-loader.php'
โโ admin-page-loader.php (line 185) โ OpinionStageAdminPageLoader::get_instance()
โโ constructor โ prepare_slug() โ load_file() โ include_once
```
The class is instantiated via `get_instance()` at file scope. The constructor calls `prepare_slug()` and `load_file()` immediately โ before WordPress performs any authentication or capability check.
### Why it's unauthenticated
`is_admin()` returns `true` for any request to a URL under `/wp-admin/`, regardless of authentication status. It checks the request path, not the user session. Endpoints such as `admin-ajax.php` and `admin-post.php` reside under `/wp-admin/` and are accessible without authentication.
The include fires during plugin initialization, before WordPress reaches any `check_ajax_referer()`, `current_user_can()`, or session validation.
---
## Endpoint
```
GET /wp-admin/admin-ajax.php?page=opinionstage-
GET /wp-admin/admin-post.php?page=opinionstage-
```
Both endpoints are accessible without authentication. The `page` parameter is processed during plugin load.
---
## Include path
The base path resolved by `plugin_dir_path(__FILE__) . 'resources/'` is:
```
/var/www/html/wp-content/plugins/social-polls-by-opinionstage/admin/resources/
```
The `.php` extension is appended automatically. A traversal payload of `../../../../../wp-config` resolves to:
```
/var/www/html/wp-content/plugins/social-polls-by-opinionstage/admin/resources/../../../../../wp-config.php
โ /var/www/html/wp-config.php
```
The depth of traversal required depends on the WordPress installation layout.
---
## Exploitation constraints
### .php extension lock
`load_file()` appends `.php` unconditionally. Only files with a `.php` extension can be included. Null byte termination (`%00`) to truncate the extension is not viable on PHP โฅ 5.3.4.
### PHP wrapper blockage
The mandatory `opinionstage-` prefix prevents the use of PHP stream wrappers. The `$_REQUEST['page']` value must contain `opinionstage-` to pass the prefix check โ there is no way to begin the value with `php://`, `data://`, `expect://`, or any other wrapper scheme while satisfying this constraint.
### Include executes, does not display
Included `.php` files are executed by the PHP interpreter. Their source code is not returned in the response. Including `wp-config.php` causes its `define()` calls to execute silently โ database credentials are loaded into memory but produce no output visible to the attacker.
### No standard .php log files
Log poisoning would require a log file with a `.php` extension, which is not a standard configuration in Apache, Nginx, or any common WordPress deployment.
---
## Chaining potential โ LFI to RCE
While standalone impact on a clean install is limited, this vulnerability becomes a reliable **unauthenticated RCE primitive** when chained with any condition that places attacker-controlled `.php` content on the filesystem.
### Chain: Arbitrary File Upload + LFI
If an attacker can upload a `.php` file to any known path on the target โ via a separate vulnerability in another plugin, theme, or WordPress core โ this LFI can include and execute it.
This chain is particularly effective because:
- Arbitrary file upload vulnerabilities in WordPress plugins are common. Multiple critical AFU CVEs are disclosed monthly across the ecosystem.
- Many AFU vulnerabilities write files to predictable paths (e.g., `wp-content/uploads/YYYY/MM/filename.php`).
- Server-side defenses against uploaded PHP execution (`.htaccess` rules denying direct PHP execution in `uploads/`, Nginx `location` blocks) are **completely bypassed**. The inclusion happens from the plugin's own process context, not from a direct HTTP request to the uploaded file. The web server never needs to route a request to the uploaded path.
### Chain: Any writable .php on disk
The AFU case is the most common, but any scenario that results in attacker-controlled `.php` content on the filesystem produces the same chain:
- SQL injection with `SELECT INTO OUTFILE` writing to a path within traversal range.
- A plugin that stores user-supplied content in `.php` files (cache engines, configuration generators, compiled template caches).
- Race conditions in temporary file creation where a `.php` temp file exists at a predictable path for a window of time.
- A previously compromised upload directory where a webshell was placed but is not directly accessible due to execution restrictions.
### Impact when chained
The combined result is **unauthenticated remote code execution** running as the web server user (`www-data`, `apache`, `nginx`). No direct URL access to the uploaded file is required, no authentication is needed, and no user interaction is involved.
---
## Standalone impact assessment
On a clean WordPress install with no other vulnerabilities present, the LFI can include arbitrary `.php` files from the local filesystem but cannot:
- Read their source code (executed, not displayed).
- Include non-PHP files (extension is forced).
- Use PHP wrappers to transform output (prefix blocks wrapper schemes).
Existing WordPress `.php` files that could be included (e.g., `wp-config.php`, core files) do not produce exploitable output when re-included in this context.
**Standalone severity: Low.**
**Chained severity: Critical (unauthenticated RCE).**
---
## Fix
Version 19.11.1 removes `admin/admin-page-loader.php` and the associated `resources/` and `helpers/` directories entirely. The admin page routing was rewritten in `src/Modules/Admin.php` using WordPress's `add_menu_page()` / `add_submenu_page()` API with proper callback registration, eliminating the dynamic include pattern.
---
## References
- [Patchstack CVE-2025-53328](https://patchstack.com/database/vulnerability/social-polls-by-opinionstage/)
- [SolidWP vulnerability report โ September 3, 2025](https://solidwp.com/blog/wordpress-vulnerability-report-september-3-2025/)
- [WordPress Plugin Directory โ social-polls-by-opinionstage](https://wordpress.org/plugins/social-polls-by-opinionstage/)